Java--CAS操作分析
CAS操作在Java中的应用很广泛,比如ConcurrentHashMap
,ReentrantLock
等,其常被用来解决独占锁对线程阻塞而导致的性能低下问题,是高效并发必备的一种优化方法.
JMM
一般的理解Java内存模型为主内存与工作内存,如下图所示:
工作内存是为了提高效率,在内部缓存了主内存中的变量,避免每次都要去主内存拿,但是变量被修改之后写回主内存的时机是不可控的,因此就会带来并发下变量一致性问题.对此Java提供了以下关键字:
volatile: 保证多线程之间的可见性,可以理解为其操作都是直接操作主内存的变量,每次读直接从主内存读,每次修改完立即写回主内存.
synchronized: 提供的锁机制在进入同步块时从主内存读取变量,同步块结束时写回变量到主内存.
synchronized所带来的新问题.
这里的分析是不考虑JVM一系列的优化措施,比如锁消除,锁粗化,自旋之类的处理优化.
排除优化措施的话synchronized本质上可以理解为悲观锁思想的实现,所谓的悲观锁认为每次访问临界区都会冲突,因此每次都需要加锁,而当没有拿到锁时线程是处于阻塞状态的.从Runnable到Blocked,然后被唤醒后再从Blocked到Runnable,这些操作耗费了不少计算机资源,因此这种悲观锁机制是并发的一种实现,但不是高效并发的实现.
CAS实现原子性操作
CAS操作大概有如下几步:
- 读取旧值为一个临时变量
- 对旧值的临时变量进行操作或者依赖旧值临时变量进行一些操作
- 判断旧值临时变量是不是等于旧值,等于则没被修改,那么新值写入.不等于则被修改,此时放弃或者从步骤1重试.
那么步骤三实际上就是比较并替换,这个操作需要是原子性的,不然无法保证比较操作之后还没写入之前有其他线程操作修改了旧值.那么这一步实际上就是CAS(CompareAndSwap),其是需要操作系统底层支持,对于操作系统会转换为一条指令,也就是自带原子性属性,对于Java中则是sun.misc.Unsafe#compareAndSwapObject
这种类型的操作.另外在Java中CAS的实现需要可见性的支持,也就是修改值后必须立即同步到主内存,否则这个修改没有意义,其他线程读取到的仍然是旧值.
CAS相比无优化下的synchronized,最大的优势就是无阻塞,也就是没了线程阻塞与唤醒的消耗,性能自然是很高.
AtomicXXX与CAS
Java中提供了AtomicXXX
一系列原子类,这里以AtomicInteger
为例,大概结构如下:
1 | public class AtomicInteger extends Number implements java.io.Serializable { |
这些类一般内部包裹一个用volatile
修饰的真实值,其解决的是内存可见性与指令重排序的问题.而原子性操作则是由unsafe
提供的一系列指令来完成.以java.util.concurrent.atomic.AtomicInteger#getAndIncrement
为例,其解决的是i++的问题,在AtomicInteger
中对于此类操作都转到了unsafe的操作.
1 | public final int getAndIncrement() { |
Unsafe中实现的策略,为了更好的理解笔者调整了一些代码顺序.正好对应上述CAS的三个步骤.
1 | public final int getAndAddInt(Object var1, long valueOffset, int addValue) { |
ReentrantLock与CAS
ReentrantLock
是Java应用层面实现的一种独占锁机制,因此比起JDK1.5之前的synchronized
有很明显的性能提升.其加锁的代码利用的就是CAS算法.其内部利用了一个state
字段,该字段为0时代表锁没有被获取,为1时则代表有线程已经获取到了锁,为n时则代表该锁被当前线程重入了n次.
1 | final void lock() { |
那么可重入机制是怎么实现的呢?
在acquire(1)
方法中会调用tryAcquire(1)
方法再次尝试获得锁,其又会转到java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
方法,在这个方法中重入的关键就是对state自增,state为n就代表重入了n次.
1 | final boolean nonfairTryAcquire(int acquires) { |
那么锁的释放实际上就是对state
字段的递减,并且当减到0时对等待队列中的线程进行唤醒.
1 | public final boolean release(int arg) { |
tryRelease
是对state字段的递减过程.
1 | protected final boolean tryRelease(int releases) { |
简单总结来说ReentrantLock
实现独占的重入锁是通过CAS对state
变量的改变来代表不同的状态来实现的,从而实现了获取锁与释放锁的高性能.
CAS总结
CAS在多线程问题中起到了什么作用?
多线程问题归根结底要解决的是可见性
,有序性
,原子性
三大问题,大家都知道JVM提供的volatile
可以保证可见性与有序性,但是无法保证原子性,换句话说 volatile + CAS实现原子性操作 = 线程安全 = 高效并发
,那么CAS就是用来实现这个操作的原子性.
CAS与乐观锁是什么关系?
乐观锁是一种思想,其认为冲突很少发生,因此只在最后写操作的时候加锁
,这里的加锁不一定是真的锁上,比如CAS一般就用来实现这一层加锁.
ABA问题
ABA问题指的是多个线程同时执行,那么开始时其获得的值都是A,当一个线程修改了A为B,第二个线程修改了B为A,那么第三个线程修改时判断A仍然是A,认为其没有修改过,因此会CAS成功.
ABA问题产生的影响取决于你的业务是否会因此受到影响.如果有影响那么解决思路一般是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
在JDK1.5之后提供了AtomicStampedReference
类来解决ABA问题,解决思路是保存元素的引用,引用相当于版本号,是每一个变量的标识,因此在CAS前判断下是否是同一个引用即可.
1 | public boolean compareAndSet(V expectedReference, |
如有错误,还请指出,以免误人子弟.
- 版权声明: 感谢您的阅读,本文由屈定's Blog版权所有。如若转载,请注明出处。
- 文章标题: Java--CAS操作分析
- 文章链接: https://mrdear.cn/posts/java_cas.html