从i++到CAS操作
前言
i++
不是一个原子操作, 原子操作的意思是, 执行代码的时候, 不会发生线程的切换.
即使i++
只有一行代码, 但是也不是一个原子操作, 主要就是三件事情, 很惭愧, 就做了一点微小的工作(
- 取值: 从内存中取值到寄存器.
- 自增: 寄存器进行
i+1
. - 回写: 将自增后的值回写到内存中.
多线程下的问题
i++
不是一个原子操作, 那么就可能在多线程下出现问题.
比如发生以下操作.
Thread 1
从内存中取值i = 0
, 在寄存器中自增i = 1
.Thread 1
在回写到内存前, 时间片结束, 切换到Thread 2
.Thread 2
从内存中取值i = 0
, 在寄存器中自增i = 1
.Thread 2
在回写到内存前, 时间片结束, 切换到Thread 1
.Thread 1
将寄存器中的i = 1
回写到内存中, 内存中i = 1
, 执行结束, 切换到Thread 2
.Thread 1
将寄存器中的i = 1
回写到内存中, 内存中i = 1
, 执行结束.
最终结果, i = 1
, 但是我们期望的是两个线程执行了i++
, 最终结果应该是i = 2
.
这时就轮到java.util.concurrent
包出场了, 下面是一个简单的例子.
1 | public class Main { |
可以看到AtomicInteger
类可以在多线程下, 输出我们预期的结果, 而普通的i++
, 则与预期结果不符.
Unsafe 类的存在
主要的代码就是incrementAndGet
这个方法. 我们可以进去看看.
1 | public class AtomicInteger extends Number implements java.io.Serializable { |
源码#L87-L92
可以看到, 里面借助了sun.misc.Unsafe
这个类, 它是一个不安全的类, 可以绕过JVM
直接操作本地内存.
所以为了不让人滥用, 调用getUnsafe()
时会判断调用Unsafe
的类是否是BootstrapClassLoader
加载的类.
我们自己写的类是由sun.misc.Launcher$AppClassLoader
加载的, 所以如果我们自己去调用getUnsafe()
, 肯定会抛出SecurityException
.
当然我们也可以通过反射获取.
1 | public class UnsafeHelper { |
深入 getAndAddInt
1 | public final class Unsafe { |
整个核心就是compareAndSwapInt
这个CAS
操作. 整个方法的逻辑就是
- 读取
Object
的value
属性在内存中的偏移量地址offset
, 写入变量v
. compareAndSwapInt
方法, 判断Object
的地址offset
的值, 是否为v
, 是则将v + delta
写入地址offset
.- 如果地址
offset
的值和变量v
不相等, 说明有其他线程修改了, 那么就再循环一次, 回到步骤1
.
总结
再深入就是c++
和汇编层次的代码了, 技术有限就不细说了(太菜), 建议查看参考资料.
总的来说, CAS
就是一个比较操作, 直接操作内存地址, 就不会发生寄存器的值来不及回写到内存中的问题.