转载

多线程与高并发13-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个线程竞争一个热点。

多线程与高并发13-Java8并发新特性

LongAdder的基本思路就是 分散热点 ,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。

这种做法和ConcurrentHashMap中的“分段锁”其实就是类似的思路。

LongAdder提供的API和 AtomicLong 比较接近,两者都能以原子的方式对long型变量进行增减。

但是 AtomicLong 提供的功能其实更丰富,尤其是 addAndGetdecrementAndGetcompareAndSet 这些方法。

addAndGet、 decrementAndGet 除了单纯的做自增自减外,还可以立即获取增减后的值,而 LongAdder 则需要做同步控制才能精确获取增减后的值。如果业务需求需要精确的控制计数,做计数比较, AtomicLong 也更合适。

另外,从空间方面考虑, LongAdder 其实是一种“空间换时间”的思想,从这一点来讲 AtomicLong 更适合。

总之,低并发、一般的业务场景下AtomicLong是足够了。如果并发量很多,存在大量写多读少的情况,那LongAdder可能更合适。适合的才是最好的,如果真出现了需要考虑到底用AtomicLong好还是LongAdder的业务场景,那么这样的讨论是没有意义的,因为这种情况下要么进行性能测试,以准确评估在当前业务场景下两者的性能,要么换个思路寻求其它解决方案。

对于 LongAdder 来说,内部有一个base变量,一个Cell[]数组。

base变量:非竞态条件下,直接累加到该变量上。

其他新增

除了新引入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

扩充知识点-Disruptor

应用背景和介绍

Disruptor是英国外汇交易公司LMAX开发的一个高性能队列,研发的初衷是解决内部的内存队列的延迟问题,而不是分布式队列。基于Disruptor开发的系统单线程能支撑每秒600万订单,2010年在QCon演讲后,获得了业界关注。

据目前资料显示:应用Disruptor的知名项目有如下的一些:Storm, Camel, Log4j2,还有目前的美团点评技术团队也有很多不少的应用,或者说有一些借鉴了它的设计机制。

Disruptor是一个高性能的线程间异步通信的框架,即在同一个JVM进程中的多线程间消息传递。

传统队列问题

在JDK中,Java内部的队列BlockQueue的各种实现,仔细分析可以得知,队列的底层数据结构一般分成三种:数组、链表和堆,堆这里是为了实现带有优先级特性的队列暂且不考虑。

在稳定性和性能要求特别高的系统中,为了防止生产者速度过快,导致内存溢出,只能选择有界队列;同时,为了减少Java的垃圾回收对系统性能的影响,会尽量选择 Array格式的数据结构。这样筛选下来,符合条件的队列就只有ArrayBlockingQueue。但是ArrayBlockingQueue是通过 加锁 的方式保证线程安全,而且ArrayBlockingQueue还存在 伪共享 问题,这两个问题严重影响了性能。

ArrayBlockingQueue的这个伪共享问题存在于哪里呢,分析下核心的部分源码,其中最核心的三个成员变量为

是在ArrayBlockingQueue的核心enqueue和dequeue方法中经常会用到的,这三个变量很容易放到同一个缓存行中,进而产生伪共享问题。

高性能的原理

引入环形的数组结构:数组元素不会被回收,避免频繁的GC,

无锁的设计:采用CAS无锁方式,保证线程的安全性

属性填充:通过添加额外的无用信息,避免伪共享问题

环形数组结构是整个Disruptor的核心所在。

首先因为是数组,所以要比链表快,而且根据我们对上面缓存行的解释知道,数组中的一个元素加载,相邻的数组元素也是会被预加载的,因此在这样的结构中,cpu无需时不时去主存加载数组中的下一个元素。而且,你可以为数组预先分配内存,使得数组对象一直存在(除非程序终止)。这就意味着不需要花大量的时间用于垃圾回收。此外,不像链表那样,需要为每一个添加到其上面的对象创造节点对象—对应的,当删除节点时,需要执行相应的内存清理操作。环形数组中的元素采用覆盖方式,避免了jvm的GC。

其次结构作为环形,数组的大小为2的n次方,这样元素定位可以通过位运算效率会更高,这个跟一致性哈希中的环形策略有点像。在disruptor中,这个牛逼的环形结构就是RingBuffer,既然是数组,那么就有大小,而且这个大小必须是2的n次方

其实质只是一个普通的数组,只是当放置数据填充满队列(即到达2^n-1位置)之后,再填充数据,就会从0开始,覆盖之前的数据,于是就相当于一个环。

每个生产者首先通过CAS竞争获取可以写的空间,然后再进行慢慢往里放数据,如果正好这个时候消费者要消费数据,那么每个消费者都需要获取最大可消费的下标。

同时,Disruptor 不像传统的队列,分为一个队头指针和一个队尾指针,而是只有一个角标(上图的seq),它属于一个volatile变量,同时也是我们能够不用锁操作就能实现Disruptor的原因之一,而且通过缓存行补充,避免伪共享问题。该指针是通过一直自增的方式来获取下一个可写或者可读数据。

原文  https://segmentfault.com/a/1190000022379294
正文到此结束
Loading...