别忘记奔跑-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

相关文章

解决pandas无法读取csv文件数据的问题

《解决pandas无法读取csv文件数据的问题》本文讲述作者用Pandas读取CSV文件时因参数设置不当导致数据错位,通过调整delimiter和on_bad_lines参数最终解决问题,并强调正确参... 目录一、前言二、问题复现1. 问题2. 通过 on_bad_lines=‘warn’ 跳过异常数据3

解决RocketMQ的幂等性问题

《解决RocketMQ的幂等性问题》重复消费因调用链路长、消息发送超时或消费者故障导致,通过生产者消息查询、Redis缓存及消费者唯一主键可以确保幂等性,避免重复处理,本文主要介绍了解决RocketM... 目录造成重复消费的原因解决方法生产者端消费者端代码实现造成重复消费的原因当系统的调用链路比较长的时

深度解析Nginx日志分析与499状态码问题解决

《深度解析Nginx日志分析与499状态码问题解决》在Web服务器运维和性能优化过程中,Nginx日志是排查问题的重要依据,本文将围绕Nginx日志分析、499状态码的成因、排查方法及解决方案展开讨论... 目录前言1. Nginx日志基础1.1 Nginx日志存放位置1.2 Nginx日志格式2. 499

kkFileView启动报错:报错2003端口占用的问题及解决

《kkFileView启动报错:报错2003端口占用的问题及解决》kkFileView启动报错因office组件2003端口未关闭,解决:查杀占用端口的进程,终止Java进程,使用shutdown.s... 目录原因解决总结kkFileViewjavascript启动报错启动office组件失败,请检查of

SpringBoot 异常处理/自定义格式校验的问题实例详解

《SpringBoot异常处理/自定义格式校验的问题实例详解》文章探讨SpringBoot中自定义注解校验问题,区分参数级与类级约束触发的异常类型,建议通过@RestControllerAdvice... 目录1. 问题简要描述2. 异常触发1) 参数级别约束2) 类级别约束3. 异常处理1) 字段级别约束

Python错误AttributeError: 'NoneType' object has no attribute问题的彻底解决方法

《Python错误AttributeError:NoneTypeobjecthasnoattribute问题的彻底解决方法》在Python项目开发和调试过程中,经常会碰到这样一个异常信息... 目录问题背景与概述错误解读:AttributeError: 'NoneType' object has no at

Spring的RedisTemplate的json反序列泛型丢失问题解决

《Spring的RedisTemplate的json反序列泛型丢失问题解决》本文主要介绍了SpringRedisTemplate中使用JSON序列化时泛型信息丢失的问题及其提出三种解决方案,可以根据性... 目录背景解决方案方案一方案二方案三总结背景在使用RedisTemplate操作redis时我们针对

Kotlin Map映射转换问题小结

《KotlinMap映射转换问题小结》文章介绍了Kotlin集合转换的多种方法,包括map(一对一转换)、mapIndexed(带索引)、mapNotNull(过滤null)、mapKeys/map... 目录Kotlin 集合转换:map、mapIndexed、mapNotNull、mapKeys、map

nginx中端口无权限的问题解决

《nginx中端口无权限的问题解决》当Nginx日志报错bind()to80failed(13:Permissiondenied)时,这通常是由于权限不足导致Nginx无法绑定到80端口,下面就来... 目录一、问题原因分析二、解决方案1. 以 root 权限运行 Nginx(不推荐)2. 为 Nginx

解决1093 - You can‘t specify target table报错问题及原因分析

《解决1093-Youcan‘tspecifytargettable报错问题及原因分析》MySQL1093错误因UPDATE/DELETE语句的FROM子句直接引用目标表或嵌套子查询导致,... 目录报js错原因分析具体原因解决办法方法一:使用临时表方法二:使用JOIN方法三:使用EXISTS示例总结报错原