volatile关键字和CAS总结
一、volatile关键字
1.1 volatile的理解
1.2 JMM内存模型之可见性
1.3 volatile不保证原子性
1.4 volatile禁止指令重排
1.5 volatile的应用(单例模式DCL代码)
二、CAS
2.1 CAS是什么
2.2 CAS底层原理
2.3 CAS缺点
2.4 ABA问题
2.4.1 AtomicReference原子引用
2.4.2 AtomicStampedReference版本号原子引用(ABA问题的解决)
-JUC
-多线程
- JUC(java.util.concurrent)
-
- 进程和线程
- - 进程:后台运行的程序(我们打开的一个软件,就是进程) - 线程:轻量级的进程,并且一个进程包含多个线程(同在一个软件内,同时运行窗口,就是线程)
- - 并发:同时访问某个东西,就是并发 - 并行:一起做某些事情,就是并行
-
- java.util.concurrent
- - java.util.concurrent.atomic - java.util.concurrent.locks
一篇文章搞懂java内存模型、JMM三大特征、volatile关键字
1.1 volatile的理解
- Volatile是Java虚拟机提供的轻量级的同步机制(三大特性)
- - 保证可见性 - 不保证原子性 - 禁止指令重排
1.2 JMM内存模型之可见性
JMM的三大特性,volatile只保证了两个,即可见性和有序性,不满足原子性
JMM是Java内存模型,也就是Java Memory Model,简称JMM,本身是一种抽象的概念,实际上并不存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值,到自己的工作内存
- 加锁和解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程:
数据传输速率:硬盘 < 内存 < < cache < CPU
上面提到了两个概念:主内存 和 工作内存
- 主内存:就是计算机的内存,也就是经常提到的8G内存,16G内存
- 工作内存:但我们实例化 new student,那么 age = 25 也是存储在主内存中
- - 当同时有三个线程同时访问 student中的age变量时,那么每个线程都会拷贝一份,到各自的工作内存,从而实现了变量的拷贝 ![img](https://img-blog.csdnimg.cn/202103312205094.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYzMDY1Ng==,size_16,color_FFFFFF,t_70)
即:JMM内存模型的可见性,指的是当主内存区域中的值被某个线程写入更改后,其它线程会马上知晓更改后的值,并重新得到更改后的值。
可见性代码验证:
package com.zb.demo;
/**
* Volatile Java虚拟机提供的轻量级同步机制
*
* 可见性(及时通知)
* 不保证原子性
* 禁止指令重排
*
*/
import java.util.concurrent.TimeUnit;
/**
* 假设是主物理内存
*/
class MyData {
//volatile int number = 0;
int number = 0;
public void addTo60() {
this.number = 60;
}
}
/**
* 验证volatile的可见性
* 1. 假设int number = 0, number变量之前没有添加volatile关键字修饰
*/
public class VolatileDemo {
public static void main(String args []) {
// 资源类
MyData myData = new MyData();
// AAA线程 实现了Runnable接口的,lambda表达式
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
// 线程睡眠3秒,假设在进行运算
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改number的值
myData.addTo60();
// 输出修改后的值
System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
}, "AAA").start();
while(myData.number == 0) {
// main线程就一直在这里等待循环,直到number的值不等于零
}
// 按道理这个值是不可能打印出来的,因为主线程运行的时候,number的值为0,所以一直在循环
// 如果能输出这句话,说明AAA线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
System.out.println(Thread.currentThread().getName() + "\t mission is over");
/**
* 最后输出结果:
* AAA come in
* AAA update number value:60
* 最后线程没有停止,并行没有输出 mission is over 这句话,说明没有用volatile修饰的变量,是没有可见性
*/
}
}
由于没有volatile修饰MyData类的成员变量number,main线程将会卡在while(myData.number == 0) {},不能正常结束。若想正确结束,用volatile修饰MyData类的成员变量number吧
1.3 volatile不保证原子性
通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。
原子性是什么?
不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要具体完成,要么同时成功,要么同时失败。
volatile不保证原子性案例演示:
class MyData {
/* *
* volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
*/
volatile int number = 0;
public void addPlusPlus() {
number ++;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
// 创建20个线程,线程里面进行1000次循环
for (int i = 0; i < 20; i++) {
new Thread(() -> {
// 里面
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
}
}, String.valueOf(i)).start();
}
// 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
// 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
while(Thread.activeCount() > 2) {
// yield表示不执行
Thread.yield();
}
// 查看最终的值
// 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
//main finally number value: 19730(答案不一定)
}
}
为什么出现数值丢失:
volatile不保证原子性理论解释:
number++在多线程下是非线程安全的。
我们可以将代码编译成字节码,可看出number++被编译成3条指令。
假设我们没有加synchronized
那么第一步就可能存在着,三个线程同时通过getfield
命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行iadd
命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行iadd
命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于20000。
volatile不保证原子性问题解决:
- 在方法上加入 synchronized 引入synchronized,虽然能够保证原子性,但是为了解决number++,而引入重量级的同步机制
- 如何不加synchronized解决number++在多线程下是非线程安全的问题?使用AtomicInteger。
import java.util.concurrent.atomic.AtomicInteger;
class MyData {
<!-- --></span>
<span class="token comment">/* *
* volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
*/</span>
<span class="token keyword">volatile</span> <span class="token keyword">int</span> number <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">addPlusPlus</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{
<!-- --></span>
number <span class="token operator">++</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
AtomicInteger atomicInteger <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">AtomicInteger</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">//默认值为0</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">addAtomic</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{
<!-- --></span>
atomicInteger<span class="token punctuation">.</span><span class="token function">getAndIncrement</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
}
public class VolatileDemo {
<!-- --></span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">main</span><span class="token punctuation">(</span>String<span class="token punctuation">[</span><span class="token punctuation">]</span> args<span class="token punctuation">)</span> <span class="token punctuation">{
<!-- --></span>
MyData myData <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">MyData</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// 创建20个线程,线程里面进行1000次循环</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator"><</span> <span class="token number">20</span><span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{
<!-- --></span>
<span class="token keyword">new</span> <span class="token class-name">Thread</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token punctuation">{
<!-- --></span>
<span class="token comment">// 里面</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> j <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> j <span class="token operator"><</span> <span class="token number">1000</span><span class="token punctuation">;</span> j<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{
<!-- --></span>
myData<span class="token punctuation">.</span><span class="token function">addPlusPlus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
myData<span class="token punctuation">.</span><span class="token function">addAtomic</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> String<span class="token punctuation">.</span><span class="token function">valueOf</span><span class="token punctuation">(</span>i<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值</span>
<span class="token comment">// 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程</span>
<span class="token keyword">while</span><span class="token punctuation">(</span>Thread<span class="token punctuation">.</span><span class="token function">activeCount</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token number">2</span><span class="token punctuation">)</span> <span class="token punctuation">{
<!-- --></span>
<span class="token comment">// yield表示不执行</span>
Thread<span class="token punctuation">.</span><span class="token function">yield</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// 查看最终的值</span>
<span class="token comment">// 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000</span>
System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>Thread<span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">"\t finally number1 value: "</span> <span class="token operator">+</span> myData<span class="token punctuation">.</span>number<span class="token punctuation">)</span><span class="token punctuation">;</span>
System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>Thread<span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">"\t finally number2 value: "</span> <span class="token operator">+</span> myData<span class="token punctuation">.</span>atomicInteger<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
}
执行结果:
1.4 volatile禁止指令重排
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为以下三种:
- 重排案例1:
public void mySort{
int x = 11;//语句1
int y = 12;//语句2
× = × + 5;//语句3
y = x * x;//语句4
}
按照正常单线程环境,执行顺序是 1 2 3 4
但是在多线程环境下,可能出现以下的顺序:
- 重排案例2:
int a,b,x,y = 0
- 重排案例3:
public class ReSortSeqDemo{
int a = 0;
boolean flag = false;
public void method01(){
a = 1;//语句1
flag = true;//语句2
}
public void method02(){
if(flag){
a = a + 5; //语句3
}
System.out.println("retValue: " + a);//可能是6或1或5或0
}
}
多线程环境中线程交替执行method01()和method02(),由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
这样就需要通过volatile来修饰,来保证线程安全性
禁止指令重排小总结:
Volatile实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象
首先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
线程安全获得保证:
工作内存与主内存同步延迟现象导致的
可见性
问题——可通过synchronized或volatile关键字解决,他们都可以使一个线程修改后的变量立即对其它线程可见
1.5 volatile的应用(单例模式DCL代码)
懒汉单例模式如何解决线程安全问题
- 用synchronized修饰方法getInstance()
2. 通过引入DCL(Double Check Lock双端检锁机制)我们能够发现,通过引入Synchronized关键字,能够解决高并发环境下的单例模式问题
但是synchronized属于重量级的同步机制,它只允许一个线程同时访问获取实例的方法,但是为了保证数据一致性,而减低了并发性,因此采用的比较少
就是在进来和出去的时候,进行检测
public static SingletonDemo getInstance() {
if(instance == null) {
// 同步代码段的时候,进行检测
synchronized (SingletonDemo.class) {
if(instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
从输出结果来看,确实能够保证单例模式的正确性,但是上面的方法还是存在问题的
DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排
原因是在某一个线程执行到第一次检测的时候,读取到 instance 不为null,instance的引用对象可能没有完成实例化。因为 instance = new SingletonDemo();可以分为以下三步进行完成:
完整代码:
/**
* SingletonDemo(单例模式)
*/
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
// a 双重检查加锁多线程情况下会出现某个线程虽然这里已经为空,但是另外一个线程已经执行到d处
synchronized (SingletonDemo.class) //b
{
//c不加volitale关键字的话有可能会出现尚未完全初始化就获取到的情况。原因是内存模型允许无序写入
if(instance == null) {
// d 此时才开始初始化
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
2.1 CAS是什么
CAS是英文单词Compare-And-Swap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS的全称是Compare-And-Swap,它是CPU并发原语
代码解释:
public class CASDemo {
public static void main(String[] args) {
// 创建一个原子类
AtomicInteger atomicInteger = new AtomicInteger(5);
/**
* 一个是期望值,一个是更新值,但期望值和原来的值相同时,才能够更改
* 假设三秒前,我拿的是5,也就是expect为5,然后我需要更新成 2019
*/
System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data: " + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t current data: " + atomicInteger.get());
}
}
上面代码的执行结果为
这是因为我们执行第一个的时候,期望值和原本值是满足的,因此修改成功,但是第二次后,主内存的值已经修改成了2019,不满足期望值,因此返回了false,本次写入失败
2.2 CAS底层原理
首先我们先看看 atomicInteger.getAndIncrement()方法的源码:
变量valueOffset表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。,通过valueOffset,直接通过内存地址,获取到值,然后进行加1的操作
unsafe类:
Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe类存在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。
Unsafe类的所有方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务
为什么Atomic修饰的包装类,能够保证原子性,依靠的就是底层的unsafe类
unsafe类的getAndAddInt方法的底层源码:
var5:就是我们从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到自己的本地内存,然后执行compareAndSwapInt()在再和主内存的值进行比较。因为线程不可以直接越过高速缓存,直接操作主内存,所以执行上述方法需要比较一次,在执行加1操作)
那么操作的时候,需要比较工作内存中的值,和主内存中的值进行比较
假设执行 compareAndSwapInt返回false,那么就一直执行 while方法,直到期望的值和真实值一样
2.3 CAS缺点
- 循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)
- 只能保证一个共享变量的原子操作
- - 当对一个共享变量执行操作时,我们可以通过循环CAS的方式来保证原子操作 - 但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性
2.4 ABA问题
ABA问题举例:
一个线程1从内存中取出A,这个时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,线程1此时还被阻塞,线程2又进行了一些操作,然后将B又变成了A,此时线程1获得资源,开始执行,但是在进行cas操作的时候发现内存中还是A,然后线程1执行成功。(说白了就是可能存在一个线程根本不知道数值发生了变化)
如何解决ABA问题: 对内存中的值加个版本号,在比较的时候除了比较值还的比较版本号。 AtomicStampedReference就是用版本号实现cas机制,再讲AtomicStampedReference之前,先讲解AtomicReference原子引用。
2.4.1 AtomicReference原子引用
原子引用其实和原子包装类是差不多的概念,就是将一个java类,用原子引用类进行包装起来,那么这个类就具备了原子性。
/**
* 原子引用
*/
class User {
String userName;
int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User z3 = new User("z3", 22);
User l4 = new User("l4", 25);
// 创建原子引用包装类
AtomicReference<User> atomicReference = new AtomicReference<>();
// 现在主物理内存的共享变量,为z3
atomicReference.set(z3);
// 比较并交换,如果现在主物理内存的值为z3,那么交换成l4
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t " + atomicReference.get().toString());
// 比较并交换,现在主物理内存的值是l4了,但是预期为z3,因此交换失败
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t " + atomicReference.get().toString());
}
}
2.4.2 AtomicStampedReference版本号原子引用(ABA问题的解决)
时间戳原子引用,来这里应用于版本号的更新,也就是每次更新的时候,需要比较期望值和当前值,以及期望版本号和当前版本号
/**
* ABA问题的解决,AtomicStampedReference
*/
public class ABADemo {
/**
* 普通的原子引用包装类
*/
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
// 传递两个值,一个是初始值,一个是初始版本号
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("============以下是ABA问题的产生==========");
new Thread(() -> {
// 把100 改成 101 然后在改成100,也就是ABA
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
try {
// 睡眠一秒,保证t1线程,完成了ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把100 改成 101 然后在改成100,也就是ABA
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "t2").start();
System.out.println("============以下是ABA问题的解决==========");
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
// 暂停t3一秒钟
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 传入4个值,期望值,更新值,期望版本号,更新版本号
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp());
}, "t3").start();
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
// 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp+1);
System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 当前最新实际版本号:" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值" + atomicStampedReference.getReference());
}, "t4").start();
}
}
参考博客: JUC