共计 3319 个字符,预计需要花费 9 分钟才能阅读完成。
这篇文章给大家聊一下 java 并发包下的 CAS 相干的原子操作,以及 Java 8 如何改良和优化 CAS 操作的性能。
因为 Atomic 系列的原子类,无论在并发编程、JDK 源码、还是各种开源我的项目中,都常常用到。
1. 场景引入,问题凸现
假如多个线程须要对一个变量不停的累加 1,比如说上面这段代码:
public class Helloworld{
private int data=0;
// 多个线程同时对 data 变量执行操作:data++
}
实际上,下面那段代码是不 ok 的,因为多个线程间接这样并发的对一个 data 变量进行批改,是线程不安全性的行为,会导致 data 值的变动不遵循预期的值来扭转。
举个例子,比如说 20 个线程别离对 data 执行一次 data++ 操作,咱们认为最初 data 的值会变成 20,其实不是。
最初可能 data 的值是 18,或者是 19,都有可能,因为多线程并发操作下,就是会有这种平安问题,导致数据后果不精确。
2. synchronized
所以,对于下面的代码,个别咱们会革新一下,让他通过加锁的形式变成线程平安的:
public class Helloworld{
private int data=0;
public synchronized void increment(){data++;}
// 多个线程同时调用办法:inrement()}
这个时候,代码就是线程平安的了,因为咱们加了 synchronized,也就是让每个线程要进入 increment() 办法之前先得尝试加锁,同一时间只有一个线程能加锁,其余线程须要期待锁。
通过这样解决,就能够保障换个 data 每次都会累加 1,不会呈现数据错乱的问题。
咱们来看看上面的图,感受一下 synchronized 加锁下的成果和气氛,相当于 N 个线程一个一个的排队在更新那个数值。
然而,如此简略的 data++ 操作,都要加一个重磅的 synchronized 锁来解决多线程并发问题,就有点杀鸡用牛刀,大材小用了。
尽管随着 Java 版本更新,也对 synchronized 做了很多优化,然而解决这种简略的累加操作,依然显得“太重了”。人家 synchronized 是能够解决更加简单的并发编程场景和问题的。
而且,在这个场景下,你要是用 synchronized,不就相当于让各个线程串行化了么?一个接一个的排队,加锁,解决数据,开释锁,下一个再进来。
3. Atomic 原子类及其底层原理
对于这种简略的 data++ 类的操作,其实咱们齐全能够换一种做法,java 并发包上面提供了一系列的 Atomic 原子类,比如说 AtomicInteger。
他能够保障多线程并发平安的状况下,高性能的并发更新一个数值。咱们来看上面的代码:
public class Helloworld{private AtomicInteger data= new AtomicInteger (0) ;
// 多个线程并发执行:data.incrementAndGet()}
大家看下面的代码,是不是很简略!多个线程能够并发的执行 AtomicInteger 的 incrementAndGet() 办法,意思就是给我把 data 的值累加 1,接着返回累加后最新的值。
这个代码里,就没有看到加锁和开释锁这一说了吧!
实际上,Atomic 原子类底层用的不是传统意义的锁机制,而是无锁化的 CAS 机制,通过 CAS 机制保障多线程批改一个数值的安全性
那什么是 CAS 呢?他的全称是:Compare and Set,也就是先比拟再设置的意思。
咱们来看下面的图,如果说有 3 个线程并发的要批改一个 AtomicInteger 的值,他们底层的机制如下:
首先,每个线程都会先获取以后的值,接着走一个原子的 CAS 操作,原子的意思就是这个 CAS 操作肯定是本人残缺执行完的,不会被他人打断。
而后 CAS 操作里,会比拟一下说,唉!大兄弟!当初你的值是不是方才我获取到的那个值啊?
如果是的话,bingo!阐明没人改过这个值,那你给我设置成累加 1 之后的一个值好了!
同理,如果有人在执行 CAS 的时候,发现自己之前获取的值跟以后的值不一样,会导致 CAS 失败,失败之后,进入一个有限循环,再次获取值,接着执行 CAS 操作!
好!当初咱们对照着下面的图,来看一下这整个过程:
首先第一步,咱们假如线程一咔嚓一下过去了,而后对 AtomicInteger 执行 incrementAndGet() 操作,他底层就会先获取 AtomicInteger 以后的值,这个值就是 0。
此时没有别的线程跟他抢!他也不论那么多,间接执行原子的 CAS 操作,问问人家说:兄弟,你当初值还是 0 吗?
如果是,阐明没人批改过啊!太好了,给我累加 1,设置为 1。于是 AtomicInteger 的值变为 1!
接着线程 2 和线程 3 同时跑了过去,因为底层不是基于锁机制,都是无锁化的 CAS 机制,所以他们俩可能会并发的同时执行 incrementAndGet() 操作。
而后俩人都获取到了以后 AtomicInteger 的值,就是 1
接着线程 2 抢先一步发动了原子的 CAS 操作!留神,CAS 是原子的,此时就他一个线程在执行!
而后线程 2 问:兄弟,你当初值还是 1 吗?如果是,太好了,阐明没人改过,我来改成 2
好了,此时 AtomicInteger 的值变为了 2。关键点来了:当初线程 3 接着发动了 CAS 操作,然而他手上还是拿着之前获取到的那个 1 啊!
线程 3 此时会问问说:兄弟,你当初值还是 1 吗?
噩耗传来!!!这个时候的值是 2 啊!线程 3 哭泣了,他说,竟然有人在这个期间改过值。算了,那我还是从新再获取一次值吧,于是获取到了最新的值,值为 2。
而后再次发动 CAS 操作,问问,当初值是 2 吗?是的!太好了,没人改,我放松改,此时 AtomicInteger 值变为 3!
上述整个过程,就是所谓 Atomic 原子类的原理,没有基于加锁机制串行化,而是基于 CAS 机制:先获取一个值,而后发动 CAS,比拟这个值被人改过没?如果没有,就更改值!这个 CAS 是原子的,他人不会打断你!
通过这个机制,不须要加锁这么重量级的机制,也能够用轻量级的形式实现多个线程平安的并发的批改某个数值。
4. Java 8 对 CAS 机制的优化
然而这个 CAS 有没有问题呢?必定是有的。比如说大量的线程同时并发批改一个 AtomicInteger,可能有很多线程会不停的自旋,进入一个有限反复的循环中。
这些线程不停地获取值,而后发动 CAS 操作,然而发现这个值被他人改过了,于是再次进入下一个循环,获取值,发动 CAS 操作又失败了,再次进入下一个循环。
在大量线程高并发更新 AtomicInteger 的时候,这种问题可能会比拟显著,导致大量线程空循环,自旋转,性能和效率都不是特地好。
于是,当当当当,Java 8 推出了一个新的类,LongAdder,他就是尝试应用分段 CAS 以及主动分段迁徙的形式来大幅度晋升多线程高并发执行 CAS 操作的性能!
在 LongAdder 的底层实现中,首先有一个 base 值,刚开始多线程来不停的累加数值,都是对 base 进行累加的,比方刚开始累加成了 base = 5。
接着如果发现并发更新的线程数量过多,就会开始实施分段 CAS 的机制,也就是外部会搞一个 Cell 数组,每个数组是一个数值分段。
这时,让大量的线程别离去对不同 Cell 外部的 value 值进行 CAS 累加操作,这样就把 CAS 计算压力扩散到了不同的 Cell 分段数值中了!
这样就能够大幅度的升高多线程并发更新同一个数值时呈现的有限循环的问题,大幅度晋升了多线程并发更新数值的性能和效率!
而且他外部实现了主动分段迁徙的机制,也就是如果某个 Cell 的 value 执行 CAS 失败了,那么就会主动去找另外一个 Cell 分段内的 value 值进行 CAS 操作。
这样也解决了线程空旋转、自旋不停期待执行 CAS 操作的问题,让一个线程过去执行 CAS 时能够尽快的实现这个操作。
最初,如果你要从 LongAdder 中获取以后累加的总值,就会把 base 值和所有 Cell 分段数值加起来返回给你。
5. 总结 & 思考
不晓得大家有没有发现这种高并发拜访下的分段解决机制,在很多中央都有相似的思维体现!因为高并发中的分段解决机制实际上是一个很常见和罕用的并发优化伎俩。
注:以上文献均摘抄自石衫架构笔记!!!