本文主要是介绍别忘记奔跑-volatile CAS ABA问题,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
一、对volatile的理解
1.JMM
2.volatile
3.你在哪些地方用到过volatile?
二、CAS你知道吗?
2.1 比较并交换
2.2 CAS底层原理 对UnSafe的理解
2.2.1 atomicInteger.getAndIncrement();
2.2.2 Unsafe
2.2.3 CAS是什么?Unsafe类+CAS思想(自旋)
2.3 CAS缺点
三、原子类AtomicInteger的ABA问题谈谈?原子更新引用知道吗?
3.1 ABA问题是怎么产生的?
3.2 原子引用
3.3 时间戳原子引用
一、对volatile的理解
1.JMM
1.1可见性、原子性、VolatileDemo代码演示可见性+原子性代码、有序性
1.2 JMM 线程安全性获得保证
工作内存与主内存同步延迟现象导致的可见性问题:可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
对于指令重排导致的可见性问题和有序性问题:可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。
2.volatile
是Java虚拟机提供的轻量级的同步机制(乞丐版的synchronized):保证可见性,不保证原子性,禁止指令重排
代码演示:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;class MyData{//MyData.java ===>MyData.class ===>jvm 字节码volatile int number = 0;public void addT060(){this.number=60;}//请注意,此时number前面是加了volatile关键字修饰的,volatile不保证原子性public void addPlusPlus(){number++;}AtomicInteger atomicInteger = new AtomicInteger();public void addAtomic(){atomicInteger.getAndIncrement();}
}/*** 1 验证volatile 的可见性* 1.1 假如int number = 0;number变量之前根本没有添加volatile关键字修饰,没有可见性* 1.2 添加了volatile,可以解决可见性问题** 2 验证volatile不保证原子性* 2.1 原子性指的是什么意思?* 不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整* 要么同时成功,要么同时失败* 2.2 volatile 不保证原子性的案例演示* 2.3 why? 数值少于20000 出现了丢失写值的情况(写覆盖)要写的时候有可能有的线程被挂起了* 2.4 如何解决原子性?* * 加sync* * 使用我们的juc下AtomicInteger*/public class VolatileDemo {public static void main(String[] args) { //main是一切方法的运行入口MyData myData = new MyData();for (int i = 1; i <= 20; i++) {new Thread(()->{for (int j=1;j<=1000;j++){myData.addPlusPlus();myData.addAtomic();}}, String.valueOf(i)).start();}//需要等待上面20个线程全部计算完成后,再用main线程取得最终的结果值看是多少?//暂停一会线程while (Thread.activeCount()>2){Thread.yield();}System.out.println(Thread.currentThread().getName()+"\t int type, finally number value:"+myData.number);//volatile如果保证原子性的话,这个值应该是2W,而执行结果却不是(某次为19468)System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type, finally number value:"+myData.atomicInteger);}public static void seeOkByVolatile() {MyData myData = new MyData();new Thread(()-> {System.out.println(Thread.currentThread().getName() + "\t come in");//暂停一会线程try {TimeUnit.SECONDS.sleep(3);//3秒钟之后执行将number加到60,可是这时候main线程还在那等着,不知道!} catch (InterruptedException e) {e.printStackTrace();}myData.addT060();System.out.println(Thread.currentThread().getName() + "\t updated number value:" + myData.number);},"AAA").start();//第2个线程就是我们的main线程while (myData.number==0){//main线程一直再这里循环等待,直到number值不再等于零}System.out.println(Thread.currentThread().getName() + "\t mission is over");}}
Java 汇编案例解释:
禁止指令重排小总结:
3.你在哪些地方用到过volatile?
3.1 单例模式DCL代码
public class SingletonDemo {private static SingletonDemo instance = null;private SingletonDemo() {System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo()");}//DCL (Double Check Lock 双端检锁机制)//以下这种写法,99.99%的正确性,底层有可能出现指令重排public static SingletonDemo getInstance() {if (instance == null) {synchronized (SingletonDemo.class){if (instance==null){instance = new SingletonDemo();}}}return instance;}public static void main(String[] args) {//单线程(main线程的操作动作.................)
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println();
// System.out.println();
// System.out.println();//并发多线程后,情况发生了很大的变化for (int i = 1; i <= 10; i++) {new Thread(() -> {SingletonDemo.getInstance();}, String.valueOf(i)).start();}}
}
3.2 单例模式volatile分析
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
在instance前面加上volatile,禁止指令重排
public class SingletonDemo {private static volatile SingletonDemo instance = null;private SingletonDemo() {System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo()");}//DCL (Double Check Lock 双端检锁机制)//以下这种写法,99.99%的正确性,底层有可能出现指令重排public static SingletonDemo getInstance() {if (instance == null) {synchronized (SingletonDemo.class){if (instance==null){instance = new SingletonDemo();}}}return instance;}public static void main(String[] args) {//单线程(main线程的操作动作.................)
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println();
// System.out.println();
// System.out.println();//并发多线程后,情况发生了很大的变化for (int i = 1; i <= 10; i++) {new Thread(() -> {SingletonDemo.getInstance();}, String.valueOf(i)).start();}}
}
二、CAS你知道吗?
2.1 比较并交换
如果线程的期望值和物理内存的真实值一样,我就修改我的更新值,返回true 进行修改。
2.2 CAS底层原理 对UnSafe的理解
2.2.1 atomicInteger.getAndIncrement();
2.2.2 Unsafe
CAS是比较并交换,它保证原子性靠的是底层的Unsafe类
2.2.3 CAS是什么?Unsafe类+CAS思想(自旋)
2.2.3.1 unsafe.getAndAddInt
CAS全称为Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
//unsafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 = this.getIntVolatile(var1, var2);//获取当前对象var1在var2地址上的值是多少,赋值给var5} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//比较并交换,比较快照值和物理内存真实值是否相等return var5; }
2.2.3.2 底层汇编
2.2.3.3 简单版小总结
CAS(CompareAndSwap)
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。
CAS应用
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
2.3 CAS缺点
1)循环时间长开销很大。
我们可以看到getAndAddInt方法执行时,有个do while
如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
2)只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
3)引出来ABA问题(重点)
三、原子类AtomicInteger的ABA问题谈谈?原子更新引用知道吗?
CAS -----> Unsafe ------> CAS底层思想 -------> ABA --------> 原子引用更新 ----------> 如何规避ABA问题
* ABA :狸猫换太子
* 解决ABA问题??? 理解原子引用+新增一种机制,那就是修改版本号(类似时间戳)
CAS不够???
3.1 ABA问题是怎么产生的?
答:CAS会导致“ABA”问题。CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是并不代表这个过程就是没有问题的。
3.2 原子引用
import java.util.concurrent.atomic.AtomicReference;class User{String userName;int age;public User(String userName, int age) {this.userName = userName;this.age = age;}@Overridepublic String toString() {return "User{" +"userName='" + userName + '\'' +", age=" + age +'}';}
}public class AtomicReferenceDemo {public static void main(String[] args) {User z3 = new User("z3",22);User li4 = new User("li4", 25);AtomicReference<User> atomicReference = new AtomicReference<>();atomicReference.set(z3);System.out.println(atomicReference.compareAndSet(z3, li4) + "\t" + atomicReference.get().toString());System.out.println(atomicReference.compareAndSet(z3, li4) + "\t" + atomicReference.get().toString());}
}
3.3 时间戳原子引用
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;public class ABADemo { //ABA问题的解决 AtomicStampedReference --->带时间戳的原子引用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(()->{atomicReference.compareAndSet(100,101);atomicReference.compareAndSet(101,100);},"t1").start(); //t1干了一次ABAnew Thread(()->{try { TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); }System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());},"t2").start();//暂停一会儿线程try { TimeUnit.SECONDS.sleep(2); }catch (InterruptedException e){ e.printStackTrace(); }System.out.println("===============以下是ABA问题的解决====================");new Thread(()->{int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);//暂停1秒钟t3线程try { TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); }atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);System.out.println(Thread.currentThread().getName() + "\t第2次版本号:" + atomicStampedReference.getStamp());atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);System.out.println(Thread.currentThread().getName() + "\t第3次版本号:" + atomicStampedReference.getStamp());},"t3").start();new Thread(()->{int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);//暂停3秒钟t4线程,保证上面的t3线程完成了一次ABA操作try { TimeUnit.SECONDS.sleep(3); }catch (InterruptedException e){ e.printStackTrace(); }boolean res = atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);System.out.println(Thread.currentThread().getName() + "\t修改成功否" + res+"\t当前最新实际版本号:"+atomicStampedReference.getStamp());System.out.println(Thread.currentThread().getName() + "\t当前实际最新值:"+atomicStampedReference.getReference());},"t4").start();}
}
执行结果如下:
这篇关于别忘记奔跑-volatile CAS ABA问题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!