一、基本概述
1.1 引入背景
例:i++。假设由线程A和B需要对i进行加1的操作。线程A和线程B会分别从主内存中读取i值到自己的工作内存中。原本相加之后的值为3,但线程A和线程B分别加1后将值刷新到主内存,发现主内存的值为2,出现错误。
1.2 解决方法
保证线程A改写共享变量的操作是原子性(不能被中断的一个或一系列操作)的。采用CAS失败重试机制。
1.3 CAS(V,E,N)
1. V代表需要读写的内存位置(工作内存)
2. E代表进行比较的预期原值(主内存)
3. N代表打算写入的新值
具体流程:
假设线程A和B读取的i为2,那么线程A使用CAS操作时,会从工作内存中取出i值2与主内存中的值进行比较,发现与主内存中的值是相同的,则执行更新操作,并把值刷新到主内存中,主内存中的i值为3。线程B需要执行i加1的操作,发现工作内存中的i值2与主内存中的i值3是不一样,更新失败。然后会重新从主内存中取值到工作内存,再执行更新操作,直到成功。
由于每次都需要和主存中的最新值进行比较,比较需要结合volatile一起使用
1.4 特点
结合CAS和volatile可以实现无锁并发
,适用于线程数较少,且多核CPU的场景下。
- CAS是基于
乐观锁
的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。 - synchronized是基于
悲观锁
的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
CAS体现的是无锁并发、无阻塞并发:
- 由于没有使用synchronized,线程不会阻塞;
- 如果竞争激烈,重试必然频繁发生,会影响效率。
二、底层原理
2.1 具体实现
底层使用的是Unsafe类
。Unsafe对象提供了操作内存、线程的的方法,Unsafe不能直接被调用,只能通过反射调用
。
Unsafe只提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt、compareAndSwapLong.
AtomicBoolean流程:
- 先把Boolean转换成整形;
- 使用compareAndSwapInt进行CAS。
源码如下,则char、float和double变量也适用类似的思路来实现:
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0; int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
AtomicInteger atomicInteger = new AtomicInteger(2020);
System.out.println(atomicInteger.compareAndSet(2020,2021));
2.2 实现类为AtomicInteger
public class AtomicInteger extends Number implements java.io.Serializable
// 表示变量value在AtomicInteger实例对象内的内存偏移量
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
} private volatile int value;
2.3 存在的问题
循环开销大
:如果线程自旋CAS长时间不成功,会给CPU带来非常大的执行开销- 一次性只能保证一个共享变量的原子性,无法保证操作多个共享变量时的原子性。
解决方法:
1. 加锁;
2. 把多个共享变量合并成一个共享变量来操作,Java提供了AtomicReference类来保证引用对象之间的原子性,即多个共享变量放在一个对象里来操作。
ABA问题
:
● 如果一个值原来是A,变成了B,最后又变成了A,那么使用CAS检查时发现它的值没有变化,但实际上却变化了。
● 解决方法: 使用版本号
,在变量前面加上版本号。使用Java提供的AtomicStampedReference来解决ABA问题。具体过程:1.先检查当前引用是否为预期引用;2.并且检查当前标志是否为预期标志(双重检查)