别忘记奔跑-volatile CAS ABA问题

2024-01-13 19:59

本文主要是介绍别忘记奔跑-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问题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/602617

相关文章

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

SpringBoot启动报错的11个高频问题排查与解决终极指南

《SpringBoot启动报错的11个高频问题排查与解决终极指南》这篇文章主要为大家详细介绍了SpringBoot启动报错的11个高频问题的排查与解决,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一... 目录1. 依赖冲突:NoSuchMethodError 的终极解法2. Bean注入失败:No qu

MySQL新增字段后Java实体未更新的潜在问题与解决方案

《MySQL新增字段后Java实体未更新的潜在问题与解决方案》在Java+MySQL的开发中,我们通常使用ORM框架来映射数据库表与Java对象,但有时候,数据库表结构变更(如新增字段)后,开发人员可... 目录引言1. 问题背景:数据库与 Java 实体不同步1.1 常见场景1.2 示例代码2. 不同操作

如何解决mysql出现Incorrect string value for column ‘表项‘ at row 1错误问题

《如何解决mysql出现Incorrectstringvalueforcolumn‘表项‘atrow1错误问题》:本文主要介绍如何解决mysql出现Incorrectstringv... 目录mysql出现Incorrect string value for column ‘表项‘ at row 1错误报错

如何解决Spring MVC中响应乱码问题

《如何解决SpringMVC中响应乱码问题》:本文主要介绍如何解决SpringMVC中响应乱码问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring MVC最新响应中乱码解决方式以前的解决办法这是比较通用的一种方法总结Spring MVC最新响应中乱码解

pip无法安装osgeo失败的问题解决

《pip无法安装osgeo失败的问题解决》本文主要介绍了pip无法安装osgeo失败的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 进入官方提供的扩展包下载网站寻找版本适配的whl文件注意:要选择cp(python版本)和你py

解决Java中基于GeoTools的Shapefile读取乱码的问题

《解决Java中基于GeoTools的Shapefile读取乱码的问题》本文主要讨论了在使用Java编程语言进行地理信息数据解析时遇到的Shapefile属性信息乱码问题,以及根据不同的编码设置进行属... 目录前言1、Shapefile属性字段编码的情况:一、Shp文件常见的字符集编码1、System编码

Spring MVC使用视图解析的问题解读

《SpringMVC使用视图解析的问题解读》:本文主要介绍SpringMVC使用视图解析的问题解读,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring MVC使用视图解析1. 会使用视图解析的情况2. 不会使用视图解析的情况总结Spring MVC使用视图

Redis解决缓存击穿问题的两种方法

《Redis解决缓存击穿问题的两种方法》缓存击穿问题也叫热点Key问题,就是⼀个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击,本文给大家介绍了Re... 目录引言解决办法互斥锁(强一致,性能差)逻辑过期(高可用,性能优)设计逻辑过期时间引言缓存击穿:给

Java程序运行时出现乱码问题的排查与解决方法

《Java程序运行时出现乱码问题的排查与解决方法》本文主要介绍了Java程序运行时出现乱码问题的排查与解决方法,包括检查Java源文件编码、检查编译时的编码设置、检查运行时的编码设置、检查命令提示符的... 目录一、检查 Java 源文件编码二、检查编译时的编码设置三、检查运行时的编码设置四、检查命令提示符