什么是原子操作
假定在多线程环境中有一个操作 A,A 操作要么不执行,要么完全执行结束(执行过程中不能被其他线程影响),那么 A 操作就是原子的。
如何实现原子操作
- synchronized 锁就是原子操作,被 synchronized 锁住的方法或代码块在执行过程中不会停下来去执行其他线程,所以 synchronized 锁是原子操作。
- 实现原子操作还可以使用处理器的 CAS 指令(现代处理器基本上都支持 CAS 指令)。
CAS 原理
现代处理器都提供了 CAS 指令(原子指令,由 CPU 保证),循环这个指令(自旋),直到成功为止。假定程序对变量 i 做 i++ 操作,CAS 操作会有如下步骤:
- 传入变量 i 的旧值(未操作之前的值,假设 i 的原始值为 0);
- 根据旧值计算出新值,执行
i++操作此时得到 i=1(此时的新值还未写入内存); - 比较内存中 i 此时的值是否等于旧值 0(在上面两步操作时,可能会有其他线程对 i 的值进行了修改,此时判断一下内存中的值是否等于旧值来判断 i 是否被其他线程修改)
- 根据步骤 3 得出的结果,如果两值相等,则把步骤 2 计算出来的新值写入内存,本次操作完成;如果两值不相等,则把内存中的值赋值给旧值,再回到步骤 1,重新开始 CAS 操作,直到执行成功为止。这便是上面提到的“自旋”。
CAS 存在的问题
-
ABA 问题
在上面的 CAS 操作步骤中的第 3 步时会根据两个值来判断是否被其他线程修改过,试想,如果有另一个线程把 i 的值修改为了 2,紧接着又把 i 的值改回了 1,此时利用第 3 步的判断就无法判断出 i 值被修改过。解决此问题可以使用版本戳的方法,在操作前定义一个版本戳,变量每被修改以此,版本戳就加 1。此线程操作完后检查版本戳是否和操作前一致,以此来判断变量 i 是否被修改过。 -
开销问题
当有很多线程同时操作同一个变量时,线程间的竞争关系很激烈,会导致步骤 4 循环次数过多,性能开销较大 -
只能保证一个共享变量进行原子操作,如果想保证多个变量进行原子操作,CAS 无法实现。可以把多个共享变量合并成一个共享变量来操作,例如将多个变量封装成一个对象,这样就可以进行 CAS 操作。
悲观锁
线程在执行某个方法或代码块时,为了防止被其他线程影响,代码执行前先上锁(synchronized),这样其他线程将无法操作,只能进行阻塞。
悲观锁可以有效解决多线程间的安全性问题,但是上锁后其他线程都要处于阻塞状态,性能较差。
乐观锁
乐观锁不会真正锁住某个方法或代码块,所有线程都可以正常执行,在真正的操作执行完后会去检查状态(常用版本戳),如果检查发现在此线程执行过程中被其他线程修改过,则终止本操作并恢复本次的操作。
乐观锁是一种思想,CAS 操作的就是乐观锁,CAS 最终判断变量是否被修改过,就是乐观锁中的检查状态。
JDK 原子操作类
JDK 提供了一系列的原子操作类,以下列举部分,详细资料请查阅 JDK 文档。
-
AtomicInteger
主要提供对以原子方式对 int 类型操作- int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger 里的 value)相加,并返回结果
- int getAndIncrement():以原子方式将当前值(AtomicInteger 里的 value)加 1,并返回加 1 前的值
- int IncrementAndAnd():以原子方式将当前值(AtomicInteger 里的 value)加 1,并返回加 1 后的值
- int getAndSet(int newValue):以原子方式将 newValue 设置为当前值,并返回实例中的旧值
-
AtomicIntegerArray
主要提供原子的方式更新数组里的整型- int addAndGet(int i,int delta):以原子方式将输入值与数组中所以 ide 元素相加,并返回相加后的值
-
AtomicReference 原子更新引用类型,使用此类可以原子更新一个对象,变相解决了 CAS 的第三个问题
-
AtomicStampedReference 利用版本戳的形式记录了每次改变后的版本号,变相解决了 CAS 的 ABA 问题,此类关心的是变量有没有被动过
-
AtomicMarkableReferene 原子更新带有标记位的引用类型,此类和 AtomicStampedReference 类似,但是此类关心的是变量被动过几次