老王:小陈啊,上一章 我们说了AtomicInteger、AtomicBoolean 的底层原理,这一篇我们就来说说Atomic系列的另一个分类AtomicReference和AtomicStampReference。
小陈:老王啊,我有个疑问啊,java不是提供了AtomicInteger、AtomicBoolean这些原子类了吗?为什么还需要有AtomicReference这东西啊?
老王:JUC虽然提供了AtomicInteger、AtomicBoolean这些基本类型的原子类,但是啊有些场景并不是仅仅修改一个变量那么简单,有可能某个需要修改几个变量 ,但是需要这个操作具有原子性,比如说我给你举例的这个例子:
(1)假如有三个变量,value1、value2、value3,我需要他们都两两相等
(2)这时将value1、value2、value3都声明成AtomicInteger原子类
(3)定义一个线程类,创建两个线程实例,每个都执行5000次value1、value2、value3的操作
(4)每次操作完成之后对比value1、value2、value3是否两两相等,如果不满足,则打印报错
java
public class MultiUpdateDemo {
// 声明三个AtomicInteger的原子类
private static AtomicInteger value1 = new AtomicInteger(0);
private static AtomicInteger value2 = new AtomicInteger(0);
private static AtomicInteger value3 = new AtomicInteger(0);
// 定义一个线程,执行3个AtomicInteger的++操作
public static class MultiUpdateThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
value1.incrementAndGet();
value2.incrementAndGet();
value3.incrementAndGet();
// 假如说执行完一次操作之后,出现
// value1、value2、value3任何两两不相等的情况
// 则打印报错
if (value1.get() != value2.get() || value1 != value3
|| value2.get() != value3.get()) {
System.out.println("不好意思,出错了!!!!!!");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
// 创建两个线程,并发的操作
MultiUpdateThread thread1 = new MultiUpdateThread();
MultiUpdateThread thread2 = new MultiUpdateThread();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
具体得到的实验结果如下:
老王:根据样例的实验结果啊,出现和很多次value1、value2、value3不相等的情况,也就是我们需要的目的没有达到,小陈啊,你知道这是什么原因吗?
小陈:老王啊,我想了一下单独对value1、value2、value3中任意一个执行incrementAndGet是原子的;但是value1.incrementAndGet()、 value2.incrementAndGet()、value3.incrementAndGet()这三个操作合起来就不是原子的。
可能thread1执行value1.incrementAndGet()操作的时候,thread2已经将三个自增操作执行完了,所以啊,thread1和thread2会相互干扰......
老王:哈哈,小陈啊,看来我没看错你啊,挺聪明的哦。像是这种情况啊要对多个变量进行操作,同时又要保证这个操作具有原子性,单独使用AtomicInteger、AtomicBoolean是做不到的。
小陈:老王,如果使用到锁不可以吗 ,比如我可以将上面的几个操作放到synchronized代码块里面:
java
// lock锁对象是一个共享变量
synchronized(lock) {
value1.incrementAndGet();
value2.incrementAndGet();
value3.incrementAndGet();
// 加入说执行完一次操作之后,出现value1、value2、value3任何两两不相等的情况
if (value1.get() != value2.get() || value1 != value3
|| value2.get() != value3.get()) {
System.out.println("不好意思,出错了!!!!!!");
}
}
老王:这种情况下使用synchronized是可以保证原子性 的,但是使用到锁啊,那并发性能就下降了很多了 ,因为在竞争激烈的时候可能会导致很多线程获取不到锁而挂起,那开销就大了,这个我们在之前的synchronized的重量级锁章节里面专门分析过了。
小陈:哦哦,原来是这样啊......
老王:嗯嗯,AtomicIntegter只能确保自己本身操作具有原子性 ,但是多个AtomicInteger操作合起来这个是确保不了的 ;可以使用synchronized将多个操作包含起来 ,但是使用到synchronized的锁操作势必会降低一部分并发的性能。
小陈:那怎样在不使用锁的情况下保证多个变量的修改是具有原子性的呢?
老王:哈哈,这个时候就需要用到Atomic给我们提供的另外一个类了,AtomicReference 。它可以将多个变量封装为对象 的多个属性,然后一次性的更新整个对象,就能cas的更新多个变量,确保原子性。
AtomicReference实现一个对象原子更新
java
public class ReferenceDemo {
// 声明一个AtomicReference,封装Demo对象的
private static AtomicReference<Demo> reference = new AtomicReference(new Demo());
// 将value1、value2、value3封装为Demo对象的属性
public static class Demo {
public int value1 = 0;
public int value2 = 0;
public int value3 = 0;
}
// 创建线程累专门执行对象的更新
public static class ReferenceThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
Demo expected;
Demo update;
// 直到CAS更新操作成功才退出
do {
expected = reference.get();
update = new Demo();
update.value1 = expected.value1 + 1;
update.value2 = expected.value2 + 1;
update.value3 = expected.value2 + 1;
} while (!reference.compareAndSet(expected, update));
// 获取CAS之后的最新对象
Demo curDemo = reference.get();
// 如果value1、value2、value3中有任意一个不相等,打印报错
if (curDemo.value1 != curDemo.value2 || curDemo.value2 != curDemo.value3
|| curDemo.value1 != curDemo.value3) {
System.out.println("不好意思,出错了!!!!!!");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
// 创建两个线程,并发的操作,验证并发操作的原子性
ReferenceThread thread1 = new ReferenceThread();
ReferenceThread thread2 = new ReferenceThread();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("运行结束了......");
}
实测结果如下:
并没有打印报错信息。它这里啊相当于把value1、value2、value3的更新操作变为了对象的更新操作,这样原本的3次操作就变为了一次CAS操作,这样就能保证原子性了。
小陈:原来是这样啊,原来是多个数据变更的操作变为一个对象变更操作 ;由于AtomicReference提供了对象替换的CAS操作,所以上面的操作就具有原子性了。
老王:是的,就是这个道理,画个图来解析它的步骤,就是这样的:
(1)将多个变量封装在一个对象 中,比如demo对象 ,封装了value1、value2、value3变量的值 ,此时三个变量均为0
(2)此时要将3个变量的值均更新为1 ,则新创建一个对象update封装value1、value2、value3的值均为1
(3)此时只需要将旧的demo对象通过cas操作替换为新的update对象即可,这样就将多个变量的更新操作变为了一个对象的cas替换操作。
老王:让我们继续,来看看AtomicReference底层有什么东西?
AtomicReference原子类底层剖析
首先看一下AtomicReference的内部属性:
java
public class AtomicReference<V> implements java.io.Serializable {
// unsafe对象
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 一个泛型对象
private volatile V value;
// value对象在AtomicReference内部的偏移量
private static final long valueOffset;
static {
try {
// 获取value相对AtomicReference的内部偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
}
看下compareAndSet方法的内部源码:
java
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
看样子跟Atomicinteger和AtomicBoolean原理是一样的,只不过AtomicInteger、AtomicBoolean底层调用的是unsafe.compareAndSwapInt方法CAS操作int的值,而这里是compareAndSwapObject是CAS操作一个内存对象而已,没啥大区别。
小陈:看样子这个AtomicReference也是挺简单的呀,唯一与AtomicInteger、AtomicBoolean不同的是:
AtomicInteger、AtomicBoolean 执行的是unsafe的compareAndSwapInt方法 ,在内存层次是直接替换一个int变量 的值;然而使用AtomicRefernce你可以创建一个新的对象 ,将所有的数据变更操作放到新对象里面,然后底层调用unsafe.compareAndSwapObject方法直接替换成新对象啊。
老王:哈哈,看来你把AtomicReference理解透了啊?也是,本来就不难嘛,就是把多个修改放在对象里面,直接CAS替换对象就是了
老王:小陈啊,学到了这里,你对CAS的理解也差不多了,可以算的是深入了,但是你知道CAS操作会有什么问题吗?
小陈:这个啊,我经常看到一些网站上说CAS操作不可避免的问题之一就是ABA问题?
老王:那你知道什么是ABA问题?
小陈:老王啊,我画个图说一下我对ABA问题的理解吧,是这样的:
(1)线程1 要执行CAS操作前 ,读取value 最新的值为A
(2)然后线程2 在这期间将内存value 的数据修改成B ,然后又修改回了A;
(3)但是线程A不知道 ,执行CAS操作的时候发现值还是A ,以为没人修改过value的值,也是就执行执行CAS操作成功了
老王:那应该怎么避免ABA这种问题?
小陈:这个应该是多增加一个维度,比如版本号 ,每一次修改数据版本号则递增1 ,然后执行CAS操作的时候多一个版本号维度判断,这样就能避免ABA问题了。
老王:是的,确实是需要多一个版本号维度去判断,那你知道Atomic原子类里面哪个类能解决这个问题吗?
小陈:啊,这个......我就不知道啊,还是老王你来讲讲吧。
老王:哈哈,好。Atomic原子类系列里面有一个类叫做AtomicStampedReference ,是AtomicReference的升级版本,看名字你就知道多了一个叫做Stamped 的东西,这东西就是版本号,也叫作邮戳。下面让我们看看AtomicStampedReference的内部结构和核心方法。
AtomicStampedReference
内部结构:
java
public class AtomicStampedReference<V> {
// 将当前对象引用和修改的版本号绑定成一个pair对
private static class Pair<T> {
// 对象引用
final T reference;
// 版本号
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
}
这里比较上面的AtomicReference多了一个stamp版本号 ,将对象和版本号绑定 在一起,形成一对pair,比较的时候同时比较对象的引用和版本号,避免ABA问题。
核心执行修改的CAS方法:
java
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
// 这里获取当前的版本号和对象
Pair<V> current = pair;
return
// 这里对比对象是否被修改过,如果被修改过,则对象引用变化了
expectedReference == current.reference &&
// 比较版本号是否一致
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
(1)获取旧的对象引用expectedRefenence
(2)执行CAS操作前,获取当前内存最新的数据
(3)对比旧的对象和当前对象的reference引用是否同一个 ,版本号stamp是否相同
(4)如果相同执行CAS操作替换,否则不一样说明有别的线程修改过数据,CAS操作失败
casPair方法:
直接调用底层的unsafe类的compareAndSwapObject方法直接替换一个对象:
java
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
让我们再画一张图来捋一捋:
老王:小陈啊,AtomicStampedReference的底层原理通过上面的代码和画图讲解,你听懂了没?
小陈:某问题了,其实就是比AtomicReference多了一个版本号stamped ,在执行CAS操作 之前对比reference的值的同时也对比版本号,如果reference一样但是stamped不一样,说明期间有人修改过但是又把值改回来了,就不允许执行CAS操作了,这样就能解决ABA的问题了。
老王:没错,理解完全正确。
小陈:老王啊,接下来我们学习什么?
老王:接下来啊,我们讲解Atomic系列中的下一章《Atomic系列之LongAdder的底层原理(分段锁提升并发性能) 》
小陈:牛逼plus......,我们下一章见。
目录
JAVA并发专题 《筑基篇》
4.什么是MESI缓存一致性协议?怎么解决并发的可见性问题?
JAVA并发专题《练气篇》
10.synchronized底层之monitor、对象头、Mark Word?
11.synchronized底层是怎么通过monitor进行加锁的?
12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁
13.synchronized怎么保证可见性、有序性、原子性?
JAVA并发专题《结丹篇》
17.AtomicInteger、AtomicBoolean的底层原理
18.AtomicReference、AtomicStampReference底层原理
19.Atomic中的LongAdder底层原理之分段锁机制
20.Atmoic系列Strimped64分段锁底层实现源码剖析
JAVA并发专题《金丹篇》
21.AQS是个啥?为啥说它是JAVA并发工具基础框架?
22.基于AQS的互斥锁底层源码深度剖析
23.基于AQS的共享锁底层源码深度剖析
24.ReentrantLock是怎么基于AQS实现独占锁的?
25.ReentrantLock的Condition机制底层源码剖析
26.CountDownLatch 门栓底层源码和实现机制深度剖析
27.CyclicBarrier 栅栏底层源码和实现机制深度剖析
28.Semaphore 信号量底层源码和实现机深度剖析
29.ReentrantReadWriteLock 读写锁怎么表示?
- ReentrantReadWriteLock 读写锁底层源码和机制深度剖析
JAVA并发专题《元神篇》并发数据结构篇
31.CopyOnAarrayList 底层分析,怎么通过写时复制副本,提升并发性能?
32.ConcurrentLinkedQueue 底层分析,CAS 无锁化操作提升并发性能?
33.ConcurrentHashMap详解,底层怎么通过分段锁提升并发性能?
34.LinkedBlockedQueue 阻塞队列怎么通过ReentrantLock和Condition实现?
35.ArrayBlockedQueued 阻塞队列实现思路竟然和LinkedBlockedQueue一样?
36.DelayQueue 底层源码剖析,延时队列怎么实现?
37.SynchronousQueue底层原理解析
JAVA并发专题《飞升篇》线程池底层深度剖析
- 什么是线程池?看看JDK提供了哪些默认的线程池?底层竟然都是基于ThreadPoolExecutor的?
39.ThreadPoolExecutor 构造函数有哪些参数?这些参数分别表示什么意思?
40.内部有哪些变量,怎么表示线程池状态和线程数,看看道格.李大神是怎么设计的?
-
ThreadPoolExecutor execute执行流程?怎么进行任务提交的?addWorker方法干了啥?什么是workder?
-
ThreadPoolExecutor execute执行流程?何时将任务提交到阻塞队列? 阻塞队列满会发生什么?
-
ThreadPoolExecutor 中的Worker是如何执行提交到线程池的任务的?多余Worker怎么在超出空闲时间后被干掉的?
-
ThreadPoolExecutor shutdown、shutdownNow内部核心流程
-
再回头看看为啥不推荐Executors提供几种线程池?
-
ThreadPoolExecutor线程池篇总结