Java8 如何优化CAS性能
问题显性
假设多个线程需要对⼀个变量不停的累加 1,⽐如说下⾯这段代码:
因为多个线程直接这样并发的对⼀个 data 变量进⾏修改,是线程不安全性的⾏为,会导致 data 值的变化不遵照预期的值来改变。
⽐如说 20 个线程分别对 data 执⾏⼀次 data++ 操作,我们以为最后 data 的值会变成 20,其实不是。最后可能 data 的值是 18,或者是 19,都有可能,因为多线程并发操作下,就是会有这种安全问题,导致数据结果不准确。
解决方案1:synchronized
对于上⾯的代码,⼀般我们会改造⼀下,让他通过加锁的⽅式变成线程安全的:
这个时候,代码就是线程安全的了,因为我们加了 synchronized,也就是让每个线程要进⼊increment() ⽅法之前先得尝试加锁,同⼀时间只有⼀个线程能加锁,其他线程需要等待锁。通过这样处理,就可以保证换个 data 每次都会累加 1,不会出现数据错乱的问题。
但是,如此简单的 data++ 操作,都要加⼀个重磅的 synchronized 锁来解决多线程并发问题,就有点杀鸡⽤⽜⼑,⼤材⼩⽤了。
虽然随着 Java 版本更新,也对 synchronized 做了很多优化,但是处理这种简单的累加操作,仍然显得 “太重了”。⼈家 synchronized 是可以解决更加复杂的并发编程场景和问题的。
⽽且,在这个场景下,你要是⽤ synchronized,不就相当于让各个线程串⾏化了么?⼀个接⼀个的排队,加锁,处理数据,释放锁,下⼀个再进来。
解决方法2
Atomic 原⼦类及其底层原理
对于这种简单的 data++ 类的操作,其实我们完全可以换⼀种做法,java 并发包下⾯提供了⼀系列的 Atomic 原⼦类,⽐如说AtomicInteger。
他可以保证多线程并发安全的情况下,⾼性能的并发更新⼀个数值。我们来看下⾯的代码:
多个线程可以并发的执⾏ AtomicInteger 的
incrementAndGet() ⽅法,意思就是给我把 data 的值累加 1,接着返回累加后最新的值。这个代码⾥,就没有看到加锁和释放锁这⼀说了吧!
实际上,Atomic 原⼦类底层⽤的不是传统意义的锁机制,⽽是⽆锁化的 CAS 机制,通过 CAS 机制保证多线程修改⼀个数值的安全性那什么是 CAS 呢?他的全称是:Compare and Set,也就是先⽐较再设置的意思。
每个线程都会先获取当前的值,接着⾛⼀个原⼦的 CAS 操作,原⼦的意思就是这个 CAS操作⼀定是⾃⼰完整执⾏完的,不会被别⼈打断。然后 CAS 操作⾥,会⽐较⼀下如果是的话,bingo!说明没⼈改过这个值,那你给我设置成累加 1 之后的⼀个值好了!
同理,如果有⼈在执⾏ CAS 的时候,发现⾃⼰之前获取的值跟当前的值不⼀样,会导致 CAS 失
败,失败之后,进⼊⼀个⽆限循环,再次获取值,接着执⾏ CAS 操作!
然后再次发起 CAS 操作,问问,现在值是 2 吗?是的!太好了,没⼈改,我抓紧改,此时AtomicInteger 值变为 3!
上述整个过程,就是所谓 Atomic 原⼦类的原理,没有基于加锁机制串⾏化,⽽是基于 CAS 机制:先获取⼀个值,然后发起 CAS,⽐较这个值被⼈改过没?如果没有,就更改值!这个 CAS是原⼦的,别⼈不会打断你!
通过这个机制,不需要加锁这么重量级的机制,也可以⽤轻量级的⽅式实现多个线程安全的并发的修改某个数值。
Java8对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 分段数值加起来返回给你。