Java并发编程(十一):Java8 新增的并发

原子操作 CAS
LongAdder
JDK1.8 时,java.util.concurrent.atomic 包中提供了一个新的原子类:LongAdder。
根据 Oracle 官方文档的介绍,LongAdder 在高并发的场景下会比它的前辈———
—AtomicLong 具有更好的性能,代价是消耗更多的内存空间。
AtomicLong 是利用了底层的 CAS 操作来提供并发性的,调用了 Unsafe 类的
getAndAddLong 方法,该方法是个 native 方法,它的逻辑是采用自旋的方式不断
更新目标值,直到更新成功。
在并发量较低的环境下,线程冲突的概率比较小,自旋的次数不会很多。但
是,高并发环境下,N 个线程同时进行自旋操作,会出现大量失败并不断自旋的
情况,此时 AtomicLong 的自旋会成为瓶颈。
这就是 LongAdder 引入的初衷——解决高并发环境下 AtomicLong 的自旋瓶
颈问题。
AtomicLong 中有个内部变量 value 保存着实际的 long 值,所有的操作都是
针对该变量进行。也就是说,高并发环境下,value 变量其实是一个热点,也就
是 N 个线程竞争一个热点。
LongAdder 的基本思路就是分散热点,将 value 值分散到一个数组中,不同
线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行 CAS 操作,
这样热点就被分散了,冲突的概率就小很多。如果要获取真正的 long 值,只要
将各个槽中的变量值累加返回。
这种做法和 ConcurrentHashMap 中的“分段锁”其实就是类似的思路。
LongAdder 提供的 API 和 AtomicLong 比较接近,两者都能以原子的方式对
long 型变量进行增减。
但是 AtomicLong 提供的功能其实更丰富,尤其是 addAndGet、
decrementAndGet、compareAndSet 这些方法。
addAndGet、decrementAndGet 除了单纯的做自增自减外,还可以立即获取
增减后的值,而 LongAdder 则需要做同步控制才能精确获取增减后的值。如果业
务需求需要精确的控制计数,做计数比较,AtomicLong 也更合适。
另外,从空间方面考虑,LongAdder 其实是一种“空间换时间”的思想,从
这一点来讲 AtomicLong 更适合。
总之,低并发、一般的业务场景下 AtomicLong 是足够了。如果并发量很多,
存在大量写多读少的情况,那 LongAdder 可能更合适。适合的才是最好的,如果
真出现了需要考虑到底用 AtomicLong 好还是 LongAdder 的业务场景,那么这样
的讨论是没有意义的,因为这种情况下要么进行性能测试,以准确评估在当前业
务场景下两者的性能,要么换个思路寻求其它解决方案。
对于 LongAdder 来说,内部有一个 base 变量,一个 Cell[]数组。
base 变量:非竞态条件下,直接累加到该变量上。
Cell[]数组:竞态条件下,累加个各个线程自己的槽 Cell[i]中。
所以,最终结果的计算应该是
在实际运用的时候,只有从未出现过并发冲突的时候,base 基数才会使用
到,一旦出现了并发冲突,之后所有的操作都只针对 Cell[]数组中的单元 Cell。
而 LongAdder 最终结果的求和,并没有使用全局锁,返回值不是绝对准确的,
因为调用这个方法时还有其他线程可能正在进行计数累加,所以只能得到某个时
刻的近似值,这也就是 LongAdder 并不能完全替代 LongAtomic 的原因之一。
而且从测试情况来看,线程数越多,并发操作数越大,LongAdder 的优势越
大,线程数较小时,AtomicLong 的性能还超过了 LongAdder。 其他新增
除了新引入 LongAdder 外,还有引入了它的三个兄弟类:LongAccumulator、
DoubleAdder、DoubleAccumulator。
LongAccumulator 是 LongAdder 的增强版。LongAdder 只能针对数值的进行加
减运算,而 LongAccumulator 提供了自定义的函数操作。
通过 LongBinaryOperator,可以自定义对入参的任意操作,并返回结果
(LongBinaryOperator 接收 2 个 long 作为参数,并返回 1 个 long)。
LongAccumulator 内部原理和 LongAdder 几乎完全一样。
DoubleAdder 和 DoubleAccumulator 用于操作 double 原始类型。
StampLock
StampedLock是Java8引入的一种新的所机制,简单的理解,可以认为它是读写
锁的一个改进版本,读写锁虽然分离了读和写的功能,使得读与读之间可以完全并
发,但是读和写之间依然是冲突的,读锁会完全阻塞写锁,它使用的依然是悲观的
锁策略.如果有大量的读线程,他也有可能引起写线程的饥饿。
而 StampedLock 则提供了一种乐观的读策略,这种乐观策略的锁非常类似于
无锁的操作,使得乐观锁完全不会阻塞写线程。
它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写。
读不阻塞写的实现思路:
在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写!即读
写之间不会阻塞对方,但是写和写之间还是阻塞的!
StampedLock 的内部实现是基于 CLH 的。
参考代码,参见 cn.enjoyedu.cha. StampedLockDemo
CompleteableFuture
Future 的不足
Future 是Java 5添加的类,用来描述一个异步计算的结果。你可以使用 isDone
方法检查计算是否完成,或者使用 get 阻塞住调用线程,直到计算完成返回结果,
你也可以使用 cancel 方法停止任务的执行。
虽然 Future 以及相关使用方法提供了异步执行任务的能力,但是对于结果
的获取却是很不方便,只能通过阻塞或者轮询的方式得到任务的结果。阻塞的方
式显然和我们的异步编程的初衷相违背,轮询的方式又会耗费无谓的 CPU 资源,
而且也不能及时地得到计算结果,为什么不能用观察者设计模式当计算结果完成
及时通知监听者呢?。
Java 的一些框架,比如 Netty,自己扩展了 Java 的 Future 接口,提供了
addListener 等多个扩展方法,Google guava 也提供了通用的扩展
Future:ListenableFuture、SettableFuture 以及辅助类 Futures 等,方便异步编程。
同时 Future 接口很难直接表述多个 Future 结果之间的依赖性。实际开发中,
我们经常需要达成以下目的:
将两个异步计算合并为一个——这两个异步计算之间相互独立,同时第二个
又依赖于第一个的结果。
等待 Future 集合中的所有任务都完成。
仅等待 Future 集合中最快结束的任务完成(有可能因为它们试图通过不同
的方式计算同一个值),并返回它的结果。
应对 Future 的完成事件(即当 Future 的完成事件发生时会收到通知,并
能使用 Future 计算的结果进行下一步的操作,不只是简单地阻塞等待操作的结
果)
CompleteableFuture
JDK1.8 才新加入的一个实现类 CompletableFuture,实现了
Future,CompletionStage两个接口。实现了 Future 接口,意味着可以像以
前一样通过阻塞或者轮询的方式获得结果。
创建
除了直接 new 出一个 CompletableFuture 的实例,还可以通过工厂方法创建
CompletableFuture 的实例
工厂方法:
Asynsc表示异步,而supplyAsync与runAsync 不同在与前者异步返回一个结果, 后者是 void.第二个函数第二个参数表示是用我们自己创建的线程池,否则采用默
认的 ForkJoinPool.commonPool()作为它的线程池。
获得结果的方法
public T get()
public T get(long timeout, TimeUnit unit)
public T getNow(T valueIfAbsent)
public T join()
getNow 有点特殊,如果结果已经计算完则返回结果或者抛出异常,否则返
回给定的 valueIfAbsent 值。
join 返回计算的结果或者抛出一个 unchecked 异常(CompletionException),它
和 get 对抛出的异常的处理有些细微的区别。
参见 cn.enjoyedu.cha.cfdemo 下 CFDemo 和 JoinAndGet
辅助方法
public static CompletableFuture allOf(CompletableFuture... cfs)
public static CompletableFuture anyOf(CompletableFuture... cfs)
allOf 方法是当所有的 CompletableFuture 都执行完后执行计算。
anyOf 方法是当任意一个 CompletableFuture 执行完后就会执行计算,计算的
结果相同。
参见 cn.enjoyedu.cha.cfdemo 下 AllofAnyOf
CompletionStage 是一个接口,从命名上看得知是一个完成的阶段,它代表
了一个特定的计算的阶段,可以同步或者异步的被完成。你可以把它看成一个计
算流水线上的一个单元,并最终会产生一个最终结果,这意味着几个
CompletionStage 可以串联起来,一个完成的阶段可以触发下一阶段的执行,接
着触发下一次,再接着触发下一次,……….。
总结 CompletableFuture 几个关键点:
1、计算可以由 Future ,Consumer 或者 Runnable 接口中的 apply,accept
或者 run 等方法表示。
2、计算的执行主要有以下
a. 默认执行
b. 使用默认的 CompletionStage 的异步执行提供者异步执行。这些方法名使
用 someActionAsync 这种格式表示。
c. 使用 Executor 提供者异步执行。这些方法同样也是 someActionAsync 这
种格式,但是会增加一个 Executor 参数。
CompletableFuture 里大约有五十种方法,但是可以进行归类,
变换类 thenApply:
关键入参是函数式接口 Function。它的入参是上一个阶段计算后的结果,返
回值是经过转化后结果。
消费类 thenAccept:
关键入参是函数式接口 Consumer。它的入参是上一个阶段计算后的结果,
没有返回值。
执行操作类 thenRun:
对上一步的计算结果不关心,执行下一个操作,入参是一个 Runnable 的实
例,表示上一步完成后执行的操作。
结合转化类:
需要上一步的处理返回值,并且 other 代表的 CompletionStage 有返回值之
后,利用这两个返回值,进行转换后返回指定类型的值。
两个 CompletionStage 是并行执行的,它们之间并没有先后依赖顺序,other
并不会等待先前的 CompletableFuture 执行完毕后再执行。
结合转化类
对于 Compose 可以连接两个 CompletableFuture,其内部处理逻辑是当第一
个 CompletableFuture 处理没有完成时会合并成一个 CompletableFuture,如果处理
完成,第二个 future 会紧接上一个 CompletableFuture 进行处理。
第一个 CompletableFuture 的处理结果是第二个 future 需要的输入参数。
结合消费类:
需要上一步的处理返回值,并且 other 代表的 CompletionStage 有返回值之
后,利用这两个返回值,进行消费
运行后执行类:
不关心这两个 CompletionStage 的结果,只关心这两个 CompletionStage 都执
行完毕,之后再进行操作(Runnable)。
取最快转换类:
两个 CompletionStage,谁计算的快,我就用那个 CompletionStage 的结果进
行下一步的转化操作。现实开发场景中,总会碰到有两种渠道完成同一个事情,
所以就可以调用这个方法,找一个最快的结果进行处理。
取最快消费类:
两个 CompletionStage,谁计算的快,我就用那个 CompletionStage 的结果进
行下一步的消费操作。
取最快运行后执行类:
两个 CompletionStage,任何一个完成了都会执行下一步的操作(Runnable)。
异常补偿类:
当运行时出现了异常,可以通过 exceptionally 进行补偿。
运行后记录结果类:
action 执行完毕后它的结果返回原始的 CompletableFuture 的计算结果或者返回
异常。所以不会对结果产生任何的作用。
运行后处理结果类:
运行完成时,对结果的处理。这里的完成时有两种情况,一种是正常执行,
返回值。另外一种是遇到异常抛出造成程序的中断。 补充:Lambda 速成
本补充章节仅为没接触过 Lambda 的同学快速入门和速查,更具体的 Lamba
的知识请自行查阅相关书籍和博客。相关代码放在 cn.enjoyedu.cha.lambda 下
现在我们有一个实体类,我们会对这个实体类进行操作。
第一步
我们想从一批 Circle 中挑选出挑选出半径为 2 的圆,于是我们写了一个方法
这样,无疑很不优雅,如果我们想挑选半径为 3 的圆,难道还要再写一个方
法?于是我们考虑将选择条件进行参数化,比如根据颜色挑选出圆或者根据半径
挑选出圆
但是,这种实现,还是有问题的,1、选择条件变化了,那么相应的方法也
要变,比如我们想挑选半径大于 3 的圆,怎么办?如果我要根据多个条件选择,
怎么办?难道把所有的条件都传入吗?于是,我们考虑定义一个挑选圆的接口,
程序进化到了第二歩
第二步
进行行为参数化,定义一个接口
在进行圆的挑选的方法里,我们把这个接口作为参数进行传递
然后,我们只要按业务需求实现接口,并传入实现类的实例即可
这种方式可以提高灵活性,但是业务上每增加一个挑选行为, 我们就需要
显式声明一个接口 ChoiceCircle 的实现类,于是我们可以考虑使用内部匿名类,
进入第三步。
第三步
在实际使用时,我们不再声明一个接口 ChoiceCircle 的实现类
匿名内部类占用代码空间较多,而且存在着模版代码,这种情况下,Lambda
表达式就可以派上用场了
所以可以把 Lambda 表达式看成匿名内部类的一个简洁写法
Lambda
在语法上,Lambda 表达式包含三个部分,参数列表,箭头,主体,比如:
(parameters) -> expression

(parameters) -> {statements;}
Lambda 表达式用在函数式接口上,所谓函数式接口,是只定义了一个抽象
方法的接口(Interface),接口中是否有默认方法,不影响。
注解@FunctionalInterface 可以帮助我们在设计函数式接口时防止出错。
我们常用的 Runnable,Callable 都是函数式接口,JDK8 中新增了几个函数式接
口:
Predicate :
包含 test 方法,接受泛型的 T,返回 boolean,可以视为断言(检查)接口
Consumer :
包含 accept 方法,接受泛型的 T,无返回,可以视为数据消费接口
Function :
包含 apply 方法,接受泛型的 T,返回 R,可以视为映射转换接口
Supplier
包含 get 方法,无输入,返回 T,可以视为创建一个新对象接口
UnaryOperator
扩展至 Function,所以这个本质上也是一个映射转换接口,只不过映
射转换后的类型保持不变
BiFunction
包含 apply 方法,接受泛型的 T、U,返回 R,可以视为复合型映射转换接口
BinaryOperator
扩展至 Function BiFunction,所以这个本质上也是一个复合型映射转
换接口,只不过映射转换后的类型保持不变
BiPredicate
包含 test 方法,接受泛型的 T,U,返回 boolean,可以视为复合型断言(检
查)接口
BiConsumer:
包含 accept 方法,接受泛型的 T,U,无返回,可以视为复合型数据消费接

同时还提供了一些为了防止自动装箱机制,而特意声明的原始类型特化的函
数式接口,比如,
在意义上,和对应的 Predicate 接口并没有差别。
函数描述符
函数式接口的抽象方法的签名基本上就是 Lambda 表达式的签名。我们将这
种抽象方法叫作函数描述符。
Runnable 接口可以看作一个什么也不接受什么也不返回(void)的函数的签
名,因为它只有一个叫作 run 的抽象方法,这个方法什么也不接受,什么也不返
回(void)。
我们可以用 () -> void 代表参数列表为空,且返回 void 的函数。这正是
Runnable 接口所代表的。我们于是可以称() -> void 是 Runnable 接口的函数描述
符。
再考察 Callable 接口和 Supplier 接口
从函数描述符来看,Callable 接口和 Supplier 接口是一样的,都是
() -> X
所以同一个 Lambda 可以同时用在这两个函数式接口上,比如:
Callable = () -> 33;
Supplier<> = () -> 33;
扩充知识点

你可能感兴趣的:(Java并发编程(十一):Java8 新增的并发)