并发专题之---Voliate引发的各种原理问题

article/2025/9/19 21:21:38

文章目录

  • 前言
  • JMM
  • voliate
    • 不保证原子性
      • 不保证原子性的解释
      • AtomicInteger解决不保证原子性的问题
      • 为什么AtomicInteger可以解决原子性问题?
        • CAS
          • CAS的内部原理
          • CAS的缺点
            • ABA问题
            • 原子引用解决ABA问题
    • 禁止指令重排
      • 多线程环境下单例模式出现的问题
        • 双端检索机制解决办法
        • 双端检索机制的隐患
        • 解决双端检索机制的隐患

前言

这里的线程安全的问题大部分都能用synchronized解决,但是本章不会讨论这个重量型的解决方法,而是去探讨更好的方案和原理.本博客也有博主自己的一些理解.
本章的目录由于过于繁杂和详细,博主自己也是做了一个流程图给大家,以便大家更好的理解和整理.
在这里插入图片描述

JMM

在这里插入图片描述

voliate

voliate是java虚拟机提供的轻量级的同步机制.他保证了可见性,不保证原子性,禁止指令重排

不保证原子性

代码案例

package com.bwie.demo;/*** 不能保证原子性* 理论来个每个线程+1000 结果为2000,但是结果却是小于2000的值*/
public class VolatileUnsafe2 {private volatile long count =0;public long getCount() {return count;}public void setCount(long count) {this.count = count;}//count进行累加public void incCount(){count++;}//线程private static class Count extends Thread{private VolatileUnsafe2 simplOper;public Count(VolatileUnsafe2 simplOper) {this.simplOper = simplOper;}@Overridepublic void run() {for(int i=0;i<10000;i++){simplOper.incCount();}}}public static void main(String[] args) throws InterruptedException {VolatileUnsafe2 simplOper = new VolatileUnsafe2();//启动两个线程Count count1 = new Count(simplOper);Count count2 = new Count(simplOper);count1.start();count2.start();Thread.sleep(50); // 这个必须添加,不然你getcount 可能永远为0System.out.println(simplOper.count);//20000?}}

不保证原子性的解释

根据JMM模型,我们可以知道,当加了voliate之后,每当在自己的内存空间写完后,需要通知其他线程自己做了修改,其他线程读取其他的线程.为什么还会有不正确的值呢?

注意:voliate 只保证可见性,重点侧重于: 每个可以拿到其他线程修改之后的最新值(但并不是意味,只要有新值就可以实时获取最新值)

在上面的列子中,如果当count = 100的时候,此时A,B 因为voliate的可见性,可以拿到主内存的新值,但是此时a,b两个线程他们做的都是100+1的操作,假如此时B线程完成了100+1的操作,设置到主内存,此时主内存为101,但是A线程还没有结束任务呀,此时他让仍然继续执行100+1的操作,相当于住内存的值 又覆盖了一遍101

有同学会问啦,不知加了voliate,会通知其他线程,读取主内存的最新值,为什么A线程还在进行100+1的操作呀,他不是应该中断这个操作,重新读取主内存的值101,然后进行101+1的操作?

如果你真的这么想,那么只能说明你对原子性和可见性含义搞蒙啦(当然,我之前 也懵了好久)

可见性:他只会保证每个线程读取的值,是上一个线程修改之后的最新值,也就是主内存的,比如count 进行100+1的操作,此时100必定是上一个线程操作之后的结果。此时他侧重的是:在我获取主内存的那一瞬间,主内存的值是最新的,注意关键词 获取主内存的那一瞬间,主内存的值最新的,就跟上面的列子一样,此时A,B他们获取到的值都是100,都是最新的,此时已经满足了可见性。

此时假如B线程完成了100+1的操作,此时主内存已经是101,A之所以不会中断100+1的操作,重新读取主内存的值,是因为可见性的特点是:获取主内存的那一瞬间,主内存的值新的,而此时他已经不是获取主内存的时间,而是在自己的内存中进行修改,此时他就会把自己的设置的101又进行一遍住内存赋值,但是A,B 两个线程下一次他去主内存获取值的时候绝对是保证是101.

那我考考大家,如果大家要解决这个问题该怎么做呀?

方案: 在进行比较的时候,看一下主内存的值,是不是我之前读取的值,如果不是,我就不把值set到主内存啦,我重新读取住内存的值,然后进行+1的操作。 聪明的你,一直是这么想的对不对!!
因为有了这个解决方案,我们的AtomicInteger 早就帮我考虑好了这个方案的封装

AtomicInteger解决不保证原子性的问题

package com.bwie.demo;import java.time.temporal.ValueRange;
import java.util.concurrent.atomic.AtomicInteger;/*** 不能保证原子性*/
public class VolatileUnsafe2 {AtomicInteger count = new AtomicInteger(0);public  void addCount(){count.getAndIncrement();}static class MyData extends Thread{private VolatileUnsafe2 simplOper;public MyData(VolatileUnsafe2 simplOper) {this.simplOper = simplOper;}@Overridepublic void run() {for (int i = 0; i <1000 ; i++) {simplOper.addCount();}}}public static void main(String[] args) throws InterruptedException {VolatileUnsafe2 volatileUnsafe2 = new VolatileUnsafe2();MyData myData = new MyData(volatileUnsafe2);MyData myData1 = new MyData(volatileUnsafe2);try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}myData.start();myData1.start();Thread.sleep(5000);System.out.println(volatileUnsafe2.count);}}

为什么AtomicInteger可以解决原子性问题?

CAS

什么是cas
CAS:又称为CompareAndSwap 比较并且交换,在我们上面的分析中,我们可以知道,因为即便加了voliate还是会有原子性的问题,其核心问题所在就是希望设置主内存值进行设置的时候,期望没有人对其修改过.而cas就是在修改的时候, 有会一个期望值,如果和期望一样的话,就说明没有修改过.此时可以进行正常设置值,他是原语,天生安全
在这里插入图片描述
案例

AtomicInteger atomicInteger = new AtomicInteger(5);System.out.println(atomicInteger.compareAndSet(5,2019+atomicInteger.get()));//此时实际值已经不是5啦 是2019System.out.println(atomicInteger.compareAndSet(5,1024+atomicInteger.get()));
输出结果truefalse
CAS的内部原理

总结就是自旋锁和Unsafe类
Unsafe类
在这里插入图片描述
在这里插入图片描述
根据上面我们可以知道 getAndAddInt是unSafe的方法,而Unsafe类天生就是java原句,他是天生原子性,他的所有方法都是安全的.

   //以下是getAndIncrement的内部源码public final int getAndIncrement() {/*** arg0:当前对象* arg1:内存偏移量* arg2:自增量* 大概意思就是当前对象的这个值为多少/return unsafe.getAndAddInt(this, valueOffset, 1);}

下面这个源码是cas的核心精华所在

public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {//get 方法,  根据当前对象的偏移量 得到具体的当前对象var5 = this.getIntVolatile(var1, var2); //得到主内存的值//var1 auomicInteger本身//var2 该对象值得引用地址//var4 需要改动的数量//var5 是用过var1和var2找出的主内存中真实值//知道修改成功啦.会跳出循环 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;}

上面的代码可以阐述为如下图的场景
在这里插入图片描述

CAS的缺点

1:因为do While 循环,所以可能循环时间长,开销比较大;
2: 只能保证一个共享变量的原子操作.因为他的var1值得就是当前对象
3:引出ABA问题(核心)

ABA问题

AB两个线程做操作,主内存的值为1,此时他们进行拷贝,他们各自的空间的值都为1
A线程把主内存的值1改为2,然后又该1, 此时B过来来修改至根据cas的期望值,他发现1就是他所期望的值,他认为并没有人对主内存进行修改过.这是不符合CAS的原理的.他只管开头和结尾,不关心中心的内容,这是不对的

原子引用解决ABA问题

先介绍下原子引用: 前面的aticInteger只能解决int类型,但是如果是对象怎么办呢?

原子引用的简单代码了解

  User user = new User("张三", 1);User user2 = new User("李四", 2);AtomicReference<User> atomicReference = new AtomicReference<>();//主物理内存为张三atomicReference.set(user);//张三变更为李四//因为user 和 前面的主物理内存相同,所以根据cas可以修改   true-->结果com.bwie.demo.User@16b4a017System.out.println(atomicReference.compareAndSet(user,user2)+"-->结果"+atomicReference.get().toString());//因为前面的操作 此时 主内存为李四,而这里他期待的是张三 所以修改失败  false-->结果com.bwie.demo.User@16b4a017System.out.println(atomicReference.compareAndSet(user,user2)+"-->结果"+atomicReference.get().toString());

时间戳的原子引用解决ABA问题
最好的办法就是在每次操作的时候加上版本号, 类似乐观锁,加入此时有两个线程T1 T2
但是每次线程修改的时候,都需加上一个版本字段,每次修改依次增加1 比如下面的列子

| 线程名称| 变量内容 |修改次数 |
|–|–|–|–|
| T1 |100 | 1 |
| T2 |100 | 1 |
此时T1做了修改吧100修改为101 ,然后在修改回来100,中间操作了2次,此时修改次数加2

| T1 |100 | 3 |
|–|–|–|–|
| T2 |100 | 1 |
此时线程线程在进行检验的时候,不仅要检查修改的内容是否和预期相同,还要检查每次修改次数

public class SingletonDemo {static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);public static void main(String[] args) {new Thread(() -> {System.out.println(Thread.currentThread().getName()+"==="+atomicReference.compareAndSet(100, 101));;System.out.println(Thread.currentThread().getName()+"==="+atomicReference.compareAndSet(101, 100));;}, "t1").start();new Thread(() -> {try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"==="+atomicReference.compareAndSet(100, 2019));}, "t2").start();}
}
输出结果
t1===true
t1===true
t2===true

上面的例子并不能解决ABA问题,接下来,我们看一下解决方案

public class SingletonDemo {static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1);public static void main(String[] args) {new Thread(() -> {int stamp = atomicStampedReference.getStamp();try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"第一次版本号"+stamp);//暂停一秒System.out.println(Thread.currentThread().getName()+"===>"+atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1));;System.out.println("第二次版本号"+atomicStampedReference.getStamp()+"实际内容"+atomicStampedReference.getReference());System.out.println(Thread.currentThread().getName()+"===>"+atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1));;System.out.println("第三次版本号"+atomicStampedReference.getStamp()+"实际内容"+atomicStampedReference.getReference());}, "t1").start();new Thread(() -> {int stamp = atomicStampedReference.getStamp();  //让他们都拿到初始的值try {Thread.sleep(8000);  // 等待,让t2看不到aba出现的过程} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"的期望值为"+stamp);System.out.println(Thread.currentThread().getName()+"===>"+atomicStampedReference.compareAndSet(100, 2019,stamp,stamp+1));System.out.println("第三次版本号"+atomicStampedReference.getStamp()+"实际内容"+atomicStampedReference.getReference());}, "t2").start();}
}t1第一次版本号1
t1===>true
第二次版本号2实际内容101
t1===>true
第三次版本号3实际内容100
t2的期望值为1
t2===>false
第三次版本号3实际内容100

禁止指令重排

什么是指令重排:计算机在执行程序的时候,为了提高性能,编译器和处理器的常常会对指令重排,
我们首先看一下例子
案例一
在这里插入图片描述
数据依赖性:在多线程的也可以能2134. 因为x和y的声明互相不影响对方,所以可以指令重排,但是不能3124, 因为必须现有x的声明,才能进行x的操作,所以此时不能进行指令重排
案例二
在这里插入图片描述
在下面的代码里面,因为指令重排很可能会出现如下的问题;
在这里插入图片描述
案例3
在这里插入图片描述
以上的代码如果出现1,2的位置互换,在多线程的环境下,此时另一个线程调用method2的时候,此时他会先读取flag=true,而此时的 a并没有赋值,此时默认为0

多线程环境下单例模式出现的问题

package com.bwie.demo;import java.time.temporal.ValueRange;
import java.util.concurrent.atomic.AtomicInteger;/*** 不能保证原子性*/
public class SingletonDemo  {public static SingletonDemo instance = null;public SingletonDemo() {System.out.println("我是构造方法");}private  static SingletonDemo getInstance(){if (instance==null){instance = new SingletonDemo ();}return instance;}public static void main(String[] args) {SingletonDemo instance = SingletonDemo.getInstance();SingletonDemo instance1 = SingletonDemo.getInstance();System.out.println(instance.equals(instance1));}
}

通过上面的案例,我们可以知道因为是单例模式,所以构造方法只会输出一次,但是多线程的环境下,则不然啦
多线程的环境下

public class SingletonDemo {public static SingletonDemo instance = null;private SingletonDemo() {System.out.println("我是构造方法");}private static  SingletonDemo getInstance() {if (instance == null) {instance = new SingletonDemo();}return instance;}public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {executorService.submit(() -> {instance =  SingletonDemo.getInstance();});}executorService.shutdown();}
}

输出结果

我是构造方法
我是构造方法
我是构造方法

双端检索机制解决办法

双端检索机制

public class SingletonDemo {public static SingletonDemo instance = null;private SingletonDemo() {System.out.println("我是构造方法");}private static  SingletonDemo getInstance() {if (instance == null) {synchronized (SingletonDemo.class) {if (instance==null){instance = new SingletonDemo();}}}return instance;}public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {executorService.submit(() -> {instance =  SingletonDemo.getInstance();});}executorService.shutdown();}
}

双端检索机制的隐患

从上面的结果我们可以看出来,好像加了双端检索机制貌似就没有出现问题啦, 而且运行检验的时候,他的确是只输出了一次构造方法吗?
但是问题真的就这么解决了吗?

DCL(双端检索机制)机制不一定安全,因为有指令重排的存在,加入voliate则可以禁止指令重排
原因是在某个线程执行到第一次检测,读取到instance为null的时候,instance对象没有完成对象的初始化,而
instance = new SingletonDemo();又可以分为三步
memory = allocate() 分配内存空间 语句1
instance(memory ) 初始化对象 语句2
instance = memory 实例指向内空间 语句3

因为语句2和语句3没有数据依赖性,所以 他们的顺序可以指令重排, 如果为132顺序的话.在对象还没有完成对象的初始化的时候,直接把null的对象指向内存空间,会导致出现null的结果

解决双端检索机制的隐患

加上voliate

public class SingletonDemo {public static volatile SingletonDemo instance = null;private SingletonDemo() {System.out.println("我是构造方法");}private static  SingletonDemo getInstance() {if (instance == null) {synchronized (SingletonDemo.class) {if (instance==null){instance = new SingletonDemo();}}}return instance;}public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {executorService.submit(() -> {instance =  SingletonDemo.getInstance();});}executorService.shutdown();}
}

http://chatgpt.dhexx.cn/article/YcxT6BqU.shtml

相关文章

voliate类型的原理及用法

voliate类型&#xff0c;机器才读取执行代码读取内存后&#xff0c;将读取的内容存到高速缓冲区里&#xff0c;在硬件里是寄存器&#xff0c;这样在一下次读取的时候就可以直接从高速缓存区里面读取&#xff08;cache&#xff09;,这也是读取速度加快的原因&#xff0c;但是如果…

voliate(轻量级的同步机制)

voliate特点: JMM内存模型三大特性&#xff1a;可见性&#xff0c;原子性&#xff0c;有序性。 1.禁止指令重排序&#xff1a; voliate实现禁止指令重排&#xff0c;避免多线程环境下程序出现乱序执行的现象。 多线程环境中线程交替执行&#xff0c;由于编译器优化重排的…

voliate工作实际应用场景

哈喽大家好&#xff0c;我是IT老哥&#xff0c;今天我们来讲讲面试必问的voliate 单线程的情况下呢&#xff0c;我们肯定用不到这个voliate 只有在多线程的情景下才能用到&#xff0c;文章结尾我会举一个经典的案例 voliate三特性 保证可见性&#xff1b;不保证复合操作的原子性…

voliate原理

voliate原理 voliate 当使用voliate关键字修饰共享变量(实例变量、静态变量)时&#xff0c;它将具备两个特性&#xff1a;可见性和禁止指令重排序优化 1.可见性 变量被修改后&#xff0c;会立即保存在主存中&#xff0c;并清除工作内存中的值。新值对于线程来说都是可见的…

C语言关键字之voliate

C语言关键字之voliate voliate的作用是作为指令关键字&#xff0c;确保本条指令不会因为编译器的优化而省略&#xff0c;而且要求每次从内存中直接读取值 当使用voliate 声明变量值时&#xff0c;系统总是重新从它所在的内存读取数据&#xff0c;直接访问变量地址&#xff0c…

java中voliate的讲解

Java并发编程&#xff1a;volatile关键字解析 volatile这个关键字可能很多朋友都听说过&#xff0c;或许也都用过。在Java 5之前&#xff0c;它是一个备受争议的关键字&#xff0c;因为在程序中使用它往往会导致出人意料的结果。在Java 5之后&#xff0c;volatile关键字才得以重…

多线程 - voliate 关键字

介绍 volatile 是 Java 中的关键字&#xff0c;用于修饰变量。它的作用是强制对被修饰的变量的写操作立即刷新到主存中&#xff0c;并强制对该变量的读操作从主存中读取最新的值&#xff0c;而不是使用缓存中的值。 作用 保证变量的可见性&#xff1a; 可见性指的是多个线程…

volatitle

被volatitle修饰的变量能够保证可见性&#xff0c;不保证原子性&#xff0c;每个线程能够获取该变量的最新值。 实现的机制&#xff1a;在写volatitle变量写到主内存时&#xff0c;指令前会加上lock&#xff0c;该指令有两个影响&#xff1a; 将当前处理器缓存行的数据写回系统…

volatile

一、不得不提的volatile volatile是个很老的关键字&#xff0c;几乎伴随着JDK的诞生而诞生&#xff0c;我们都知道这个关键字&#xff0c;但又不太清楚什么时候会使用它&#xff1b;我们在JDK及开源框架中随处可见这个关键字&#xff0c;但并发专家又往往建议我们远离它。比如…

voliate关键字原理

被volatile修饰的变量在编译成字节码文件时会多个lock指令&#xff0c;该指令在执行过程中会生成相应的内存屏障&#xff0c;以此来解决可见性跟重排序的问题。 预备知识 指令重排序 为什么到指令重排序&#xff1a;一般来说&#xff0c;处理器为了提高程序运行效率&#xff…

voliate关键字

voliate关键字详解&#xff1a; 1.内存模型相关概念 物理计算机内存访问图&#xff1a; 任何计算都是在CPU内处理的&#xff0c;那么也就必须涉及到数据读写&#xff0c;但是CPU每次都要和主内存交互读写数据效率太低了&#xff0c;于是有了高速缓存。在程序运行时&#xff…

java面试题:voliate底层原理——详解

1. voliate底层原理 1.1 voliate变量的特点 可见性&#xff1a; 当一个线程修改了声明为volatile变量的值&#xff0c;新值对于其他要读该变量的线程来说是立即可见的。有序性&#xff1a; volatile变量的所谓有序性也就是被声明为volatile的变量的临界区代码的执行是有顺序的…

FSM有限状态机(三段式)-Verilog实现

一. 状态机理论基础 状态机基本概念&#xff1a; 状态机由状态寄存器和组合逻辑电路构成&#xff0c;能够根据控制信号按照预先设定的状态进行状态转移&#xff0c;是协调相关信号动作、完成特定操作的控制中心。 以上是标准解释&#xff0c;其实状态机就是为了解决比如IIC协…

Verilog描述有限状态机(一段式、二段式、三段式)

有限状态机&#xff08;FSM&#xff09;的输出取决于过去状态以及当前输入&#xff0c;是时序逻辑电路。适合描述那些发生有先后顺序或者有逻辑规律的事情&#xff0c;状态机的本质就是对具有逻辑顺序或时序规律的事件进行描述的一种方法&#xff0c;广泛用于多种场合。 因此&…

以爱情规律为例,浅谈三段式描述状态机

目录 基础概念介绍 状态机的要素 FSM的分类 Verilog描述状态机的方法 一段式描述 两段式描述 三段式描述 结语 基础概念介绍 今夜闲来无事&#xff0c;忽然想到最近准备复习一下Verilog语法&#xff0c;所以就侃一侃状态机吧&#xff01; 状态机根据状态的数量可以分为两…

三段式状态机-FSM

三段式代码多&#xff0c;但是有时钟同步&#xff0c;延时少&#xff0c;组合逻辑跟时序逻辑分开并行出错少。 (1)同步状态转移 &#xff08;2&#xff09;当前状态判断接下来的状态 &#xff08;3&#xff09;动作输出 如果程序复杂可以不止三个always 。always 后常接cas…

一段式、两段式和三段式状态机

这里写自定义目录标题 一段式两段式三段式 这张图片是mealy型状态机的结构&#xff0c;Moore型类似&#xff0c;输出与输入无关。 一段式 module moduleName (input clk,input rst_n,input in,output reg [3:0] out );localparam s14d0, s24d1, s34d2, s44d3, s54d4; reg [3:…

FPGA开发基础之三段式状态机

状态机由状态寄存器和组合逻辑电路构成&#xff0c;能够根据控制信号按照预先设定的状态进行状态转移&#xff0c;程序的运行其本质也是状态机&#xff0c;根据输入完成输出&#xff0c;得到新的状态。 在平时硬件电路的设计中经常需要用到状态机&#xff0c;例如CPU的取指、译…

关于三段式状态机第三段是组合逻辑还是时序逻辑的问题?

由于本人一直以来&#xff0c;用的三段式状态机&#xff0c;第三段写法都是组合逻辑写法&#xff0c;但是近期有小伙伴面试小公司&#xff0c;写到状态机的第三段时候&#xff0c;按照我一直用到的组合逻辑来写第三段&#xff0c;提供输出&#xff0c;被提出了质疑&#xff0c;…

【状态机设计】Moore、Mealy状态机、三段式、二段式、一段式状态机书写规范

目录 状态机介绍 状态机类型 Moore 型状态机 Mealy 型状态机 状态机设计流程 自动售卖机 状态机设计&#xff1a;3 段式&#xff08;推荐&#xff09; 实例 实例 状态机修改&#xff1a;2 段式 实例 状态机修改&#xff1a;1 段式&#xff08;慎用&#xff09; 实…