Java 常用原子类
并发编程中很容易出现并发安全的问题,一个很简单的例子,变量 i=1
,多个线程执行 i++
操作,就有可能获取不到正确的值。可以通过 synchronized
进行控制来达到线程安全的目的,但是 synchronized
是采用的是悲观锁策略(有竞争的情况),并不是高效的解决方案。在 J.U.C
下的 atomic
包提供了一系列的操作简单、性能高效,并能保证线程安全的类去更新基本类型变量、数组元素、引用类型以及更新对象中的字段类型。atomic
包下的这些类都是采用的是乐观锁策略去原子更新数据,使用 CAS
操作具体实现。
CAS 操作
CAS 操作是一种乐观锁策略,假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?CAS(Compare And Swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。
CAS 的过程可以通俗的理解为 CAS(V,O,N)
,三个值分别为:
- V 内存地址存放的实际值
- O 预期的值(旧值)
- N 更新的新值
当 V 和 O 相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值 O 就是目前来说最新的值了,可以将新值 N 赋值给 V。反之,V 和 O 不相同,表明该值已经被其他线程改过了则该旧值 O 不是最新版本的值了,所以不能将新值 N 赋给 V,返回 V 即可。
当多个线程使用 CAS 操作一个变量时,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程。
CAS 的实现需要硬件指令集的支撑,在 JDK1.5 后虚拟机才可以使用处理器提供的 CMPXCHG 指令实现。
Synchronized vs CAS
Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。CAS 并不是直接将线程挂起,当 CAS 操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步,这是两者主要的区别。
CAS 的问题
ABA 问题
因为 CAS 会检查旧值有没有变化,这里存在这样一个常见的问题。比如一个旧值 A 变为了成 B,然后再变成 A,刚好在做 CAS 时检查发现旧值并没有变化依然为 A,但是实际上的确发生了变化。
解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径 A->B->A 就变成了 1A->2B->3A。
自旋时间过长
使用 CAS 是非阻塞同步,也就是说不会将线程挂起,会自旋(死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果 JVM 能支持处理器提供的 pause 指令,那么在效率上会有一定的提升。
原子更新基本类型
原子更新基本类型的工具类主要有:
- AtomicBoolean:以原子更新的方式更新 boolean 类型变量
- AtomicInteger:以原子更新的方式更新 Integer 类型变量
- AtomicLong:以原子更新的方式更新 Long 类型变量
- …
这几个类的用法基本一致,以 AtomicInteger 为例总结常用的方法
addAndGet(int delta)
:以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果getAndSet(int newValue)
:将实例中的值更新为新值,并返回旧值incrementAndGet()
:以原子的方式将实例中的原值进行加 1 操作,并返回最终相加后的结果getAndIncrement()
:以原子的方式将实例中的原值进行加 1 操作,返回的是自增前的旧值
为了能够弄懂实现原理,以 getAndIncrement()
方法为例,来看下源码:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
该方法实际上是调用了 unsafe
实例的 getAndAddInt
方法,unsafe
实例的获取是通过 UnSafe
类的静态方法获取:
private static final Unsafe unsafe = Unsafe.getUnsafe();
Unsafe
类在 sun.misc
包下,类提供了一些计算机底层操作,原子操作类主要是通过 Unsafe
类提供的 compareAndSwapInt
、compareAndSwapLong
等一系列提供 CAS 操作的方法来进行实现。