多线程 - voliate 关键字

article/2025/9/19 21:37:46

介绍

volatile 是 Java 中的关键字,用于修饰变量。它的作用是强制对被修饰的变量的写操作立即刷新到主存中,并强制对该变量的读操作从主存中读取最新的值,而不是使用缓存中的值。

作用

保证变量的可见性:

可见性指的是多个线程之间对共享变量的修改能否被及时地通知到其他线程,也就是说,当一个线程修改了共享变量的值时,其他线程能够立即看到这个变化。如果共享变量的可见性不能得到保证,就可能出现数据不一致的情况。

当一个变量被 volatile 修饰时,任何对该变量的写操作都会立即刷新到主存中,而任何对该变量的读操作都会从主存中读取最新的值。这样可以保证多个线程之间对该变量的读写操作都是可见的。

禁止指令重排:

指令重排指的是编译器或处理器为了提高程序运行效率而对指令序列进行重新排序的过程。在多线程环境下,指令重排可能会导致程序的执行结果与预期结果不一致。因此,为了保证程序的正确性,需要使用 volatile 关键字或 synchronized 关键字来禁止指令重排。具体来说,当一个变量被volatile修饰时,编译器和处理器会插入一些内存屏障,保证指令的执行顺序不会被打乱。

内存屏障是一种特殊的CPU指令,它可以保证在执行到内存屏障之前的所有指令都已经执行完成,并且其结果已经被写入内存中。在执行内存屏障之后的指令,必须等待前面的指令执行完成后,才能开始执行。这样可以避免由于 CPU 的乱序执行导致的指令重排和内存操作的乱序问题。synchronized 和 volatile 在实现上都使用了内存屏障来保证线程安全性。synchronized 使用了一种特殊的内存屏障——“内存锁定”,它可以保证线程在获取锁之前,所有对共享变量的修改操作都已经被刷新到主内存中,同时在释放锁之后,所有对共享变量的修改操作都已经对其他线程可见。而volatile使用了“写屏障”和“读屏障”,它们分别保证写操作和读操作在指令执行时都不会受到指令重排的影响,从而保证了线程对共享变量的访问操作的可见性和禁止指令重排。

编译器和处理器为了提高程序的执行效率,可能会对指令进行重排。使用volatile关键字可以禁止这种优化,保证指令的执行顺序不会被打乱。

不能保证原子性

原子性指的是一个操作是不可被中断的,要么全部执行,要么全部不执行。在多线程环境下,如果某个操作不是原子性的,就可能会出现多个线程同时修改同一个变量的情况,从而导致数据不一致。

如果需要保证原子性,可以使用 synchronized 关键字或者juc.atomic包中的原子类。

可见性、有序性、原子性

可见性、有序性、原子性三个特性都是线程安全的必要条件,三个特性缺一不可。因此,保证其中任何一个特性都不能认为是线程安全的全部,而只有同时保证三个特性才能实现真正的线程安全。

synchronized 关键字是一种保证线程安全的机制,它可以实现原子性和有序性,同时也可以保证可见性,因此使用 synchronized 可以保证线程安全,但是会影响程序的性能。synchronized 通过独占锁来保证同步代码块的原子性和有序性,同时在获取和释放锁的过程中,会使用内存屏障来保证可见性和有序性。

在Java 5之后,JDK 引入了 volatile 关键字,它可以保证可见性和有序性,但是无法保证原子性。volatile 关键字只能保证共享变量的读写操作是原子的,但是不能保证复合操作的原子性,如递增操作。因此,在需要保证原子性的情况下,仍然需要使用 synchronized 关键字。

所以,使用 synchronized 可以保证线程安全,并且同时保证了可见性、有序性和原子性,而使用 volatile 只能保证可见性和有序性,无法保证原子性。

不会导致线程阻塞

线程阻塞指的是线程暂停执行,等待某个条件得到满足后再继续执行的一种状态。线程阻塞可以分为两种情况:

主动阻塞:主动阻塞是指线程在执行过程中,调用了某个方法或者操作,而该方法或者操作需要等待某个条件的满足才能继续执行。例如,线程调用了sleep方法,就会主动阻塞一段时间;线程调用了wait方法,就会主动阻塞,等待其他线程的唤醒。被动阻塞:被动阻塞是指线程在执行过程中,由于某些原因(如I/O操作、锁竞争等)而无法继续执行,进入阻塞状态。例如,线程等待某个资源的释放,就会被动阻塞。

无论是主动阻塞还是被动阻塞,都会导致线程暂停执行,直到某个条件得到满足或者等待时间到期,才会重新被唤醒并继续执行。因此,在多线程编程中需要注意线程的阻塞状态,避免出现死锁、饥饿等问题。

volatile 关键字不会阻塞线程,它只是保证对被修饰的变量的读写操作都从主存中进行,而不是使用缓存中的值。

使用场景

1、多个线程之间共享一个变量,并且其中一个线程对该变量的修改需要立即对其他线程可见时。

/*** 将myVariable变量声明为volatile类型。* 在incrementmyVariable()方法中,我们对myVariable的值进行了修改,而不是使用++运算符。* 这是因为++运算符不是一个原子操作,它实际上包含读取变量值、增加变量值和写回变量值这三个步骤,* 如果多个线程同时执行这些步骤,可能会导致值的不一致。* 因此,使用了一个简单的加法操作来对myVariable进行修改,这样就可以确保线程安全。* @author Administrator*/
public class MyVolatile1 {private volatile int myVariable = 0;public void incrementMyVariable() {myVariable = myVariable + 1;}public int getMyVariable() {return myVariable;}
}

2、多个线程之间共享一个变量,并且该变量的值可能会被多个线程同时修改时。

/*** 将myVariable变量声明为volatile类型。由于多个线程可能同时修改该变量的值,* 需要使用synchronized关键字来保护它。* incrementMyVariable()、decrementMyVariable()、getMyVariable()方法中,* 都使用了synchronized关键字来确保对共享变量的访问是原子的。这样就可以确保线程安全。**** 为什么myVariable = myVariable + 1;是线程安全;myVariable ++不是?* myVariable++操作实际上包含了三个步骤:* 1、读取myVariable的值、* 2、将其增加1、* 3、将结果写回到myVariable。* 如果有多个线程同时执行myVariable++操作,就会出现竞态条件,导致myVariable的值不确定。** 例如,假设myVariable的初始值为0,有两个线程同时执行myVariable++操作,它们可能会执行以下步骤:** 线程1读取myVariable的值为0。* 线程2读取myVariable的值为0。* 线程1将myVariable的值增加1,结果为1。* 线程2将myVariable的值增加1,结果也为1。* 线程1将结果1写回到myVariable。* 线程2将结果1写回到myVariable。* 最终,myVariable的值为1,而不是预期的2。这就是竞态条件导致的结果。** 相比之下,myVariable = myVariable + 1;* 操作只包含两个步骤:读取myVariable的值和将其增加1。由于这两个步骤是在一个原子操作中完成的,所以不会出现竞态条件,从而保证了线程安全。** 因此,当我们需要对共享变量进行增加操作时,建议使用myVariable = myVariable + 1;这种形式,而不是myVariable++操作,以确保线程安全。* @author Administrator*/
public class MyVolatile2 {private volatile int myVariable = 0;public synchronized void incrementMyVariable() {myVariable = myVariable + 1;}public synchronized void decrementMyVariable() {myVariable = myVariable - 1;}public synchronized int getMyVariable() {return myVariable;}
}

实现原理

使用内存屏障:当一个变量被 volatile 修饰时,编译器和处理器会插入一些内存屏障,保证指令的执行顺序不会被打乱。

使用缓存一致性协议:当一个线程对一个 volatile 变量进行写操作时,处理器会发送一个信号通知其他处理器该变量的值已经被修改,其他处理器会强制将该变量的缓存行失效,从而保证其他线程读取该变量时能够读取到最新的值。

happens-before

happens-before,用于描述多线程程序中的事件顺序关系。如果一个事件 A happens-before 另一个事件B,那么A对共享变量的写入操作对B的读取操作是可见的,也就是说,B 可以看到 A 写入的值。happens-before 关系可以通过以下方式建立:
1、程序顺序规则:在一个线程中,按照程序顺序,前面的操作 happens-before 于后面的操作。
2、volatile变量规则:对一个 volatile 变量的写操作 happens-before 于后续对该变量的读操作。
3、锁定规则:一个解锁操作 happens-before 于后续的加锁操作。
4、传递性:如果A happens-before B,B happens-before C,那么A happens-before C。

局限性

不能保证原子性:volatile关键字只能保证可见性和禁止指令重排,不能保证原子性。如果需要保证原子性,可以使用synchronized 关键字或者 juc.atomic包中的原子类。不能替代锁:volatile 关键字只能保证可见性,不能保证同步性。如果需要保证同步性,仍然需要使用 synchronized 关键字或者其他锁机制。 对于复合操作的保证:当多个变量之间存在依赖关系时,使用 volatile 关键字不能保证操作的原子性和一致性。此时仍然需要使用 synchronized 关键字或者其他锁机制来保证操作的一致性。

和 synchronized 关键字比较

volatile关键字只能修饰变量,而synchronized关键字可以修饰方法或代码块。
volatile关键字不能保证原子性,而synchronized关键字可以保证原子性。
volatile关键字不会阻塞线程,而synchronized关键字可能会导致线程阻塞。
volatile关键字只能保证可见性和禁止指令重排,而synchronized关键字可以保证同步性和原子性。
volatile关键字用于保证可见性和禁止指令重排,但是不能保证原子性,不能替代锁机制。
synchronized关键字用于保证同步性和原子性,但是不能保证可见性。synchronized关键字通过获取锁来保证同一时刻只有一个线程可以执行临界区代码,从而保证线程安全。

和 Atomic 类比较

volatile 关键字只能保证可见性和禁止指令重排,不能保证原子性。
Atomic 类可以保证可见性和原子性,但是不能替代锁机制。Atomic 类通过CAS操作实现原子性,当多个线程同时对一个变量进行修改时,只有一个线程能够成功执行 CAS 操作,从而保证操作的原子性。

和 final 关键字比较

volatile关键字用于保证可见性和禁止指令重排,但是不能保证原子性,不能替代锁机制。
final关键字用于定义常量,一旦定义就不能再修改它的值。final关键字可以保证线程安全,因为一个final变量的值一旦被初始化之后就不会再被修改,所以不会存在多线程之间的竞争问题。

和 ThreadLocal 关键字比较

volatile 关键字用于保证可见性,即多个线程之间能够正确地访问共享变量,避免出现数据不一致的情况。

ThreadLocal 用于实现线程本地变量,即每个线程都有自己独立的变量副本,线程之间不会相互干扰。ThreadLocal 主要解决的是多线程中数据的隔离问题,可以让每个线程都拥有自己独立的变量副本,从而避免出现数据不一致的情况。

失效场景

1、如果变量的值只会被单个线程修改,而其他线程只会读取这个值,那么就算不使用volatile关键字,这个程序也是线程安全的。
在这种情况下,即使不使用volatile关键字,多个线程之间也不会产生竞争条件,因为每个线程都是独立地读取和修改变量的值。因此,volatile关键字在这种情况下并不会产生任何影响。

// 场景1:如果变量的值只会被单个线程修改,那么即使不使用volatile关键字,程序也是线程安全的。public static void testScenario1() throws InterruptedException {/*** 即使使用 newCachedThreadPool 创建的线程池,每个任务也只会被单个线程执行。* 因为线程池会维护一定数量的线程,当有新任务时会从线程池中取出一个空闲的线程执行,* 当任务执行完毕后,该线程就会重新归还到线程池中等待下一个任务的到来。** 在这个例子中,虽然没有显式地指定线程数,但是线程池内部会根据需要自动创建和维护线程。* 在执行任务时,线程池会选取一个空闲的线程去执行任务,而其他线程都会被阻塞等待。* 因此,虽然任务可能被多个线程执行,但每个线程只会执行部分任务,任务的修改操作也只会被单个线程执行,因此这个程序是线程安全的。*///ExecutorService executorService = Executors.newCachedThreadPool();ExecutorService executorService = Executors.newSingleThreadExecutor();executorService.execute(() -> {for (int i = 0; i < 10000; i++) {count++;}});executorService.shutdown();executorService.awaitTermination(1, TimeUnit.SECONDS);System.out.println("Scenario1: count=" + count);}

2、多个线程对变量进行复合操作,例如 i++ 操作,这种操作不是原子性操作,volatile关键字无法保证操作的一致性。

在这种情况下,如果多个线程同时对变量进行复合操作,例如i++操作,会导致竞争条件的产生,从而导致线程安全问题。虽然volatile关键字可以保证变量的可见性,但是它无法保证操作的原子性,因此使用volatile关键字并不能解决这种情况下的线程安全问题。

如果需要保证操作的原子性,可以使用synchronized关键字或者juc.atomic包中提供的原子类。

// 场景2:多个线程对变量进行复合操作,volatile关键字无法保证操作的一致性。public static void testScenario2() throws InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {executorService.execute(() -> {for (int j = 0; j < 1000; j++) {count++;}});}executorService.shutdown();executorService.awaitTermination(1, TimeUnit.SECONDS);System.out.println("Scenario2: count=" + count);}

3、多个线程之间对变量的操作存在依赖关系,但是依赖关系没有被正确处理,例如线程A对变量 i 进行操作后,线程 B 读取了i的值,但是没有正确处理线程 A 对 i 的操作,导致线程 B 读取到了错误的值。

在这种情况下,volatile关键字可以保证变量的可见性,但是它无法保证多个线程之间的操作顺序。如果多个线程之间的操作存在依赖关系,那么需要使用同步机制,例如synchronized关键字或者 juc 包中提供的同步工具,来保证操作的顺序和一致性。 B 读取了i的值,但是没有正确处理线程 A 对 i 的操作,导致线程 B 读取到了错误的值。

 // 场景3:多个线程之间对变量的操作存在依赖关系,但是依赖关系没有被正确处理,使用volatile关键字也无法保证正确性。public static void testScenario3() throws InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(2);executorService.execute(() -> {for (int i = 0; i < 1000; i++) {count++;}});executorService.execute(() -> {for (int i = 0; i < 1000; i++) {if (count % 2 == 0) {count++;}}});executorService.shutdown();executorService.awaitTermination(1, TimeUnit.SECONDS);System.out.println("Scenario3: count=" + count);}

测试

public class VolatileTest {private static volatile int count = 0;public static void main(String[] args) throws InterruptedException {// 测试场景1:单个线程修改变量值testScenario1();// 测试场景2:多个线程对变量进行复合操作testScenario2();// 测试场景3:多个线程之间存在依赖关系testScenario3();}
}

结果

在这里插入图片描述


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

相关文章

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; 实…

状态机的描述方法案例分析(一段式、二段式、三段式)

上篇博文讲了&#xff1a;FPGA中有限状态机的状态编码采用格雷码还是独热码&#xff1f; 那篇博文讲了状态机的状态编码是用格雷码还是独热码的问题&#xff0c;以及两者之间的优劣。状态机的描述方式采用的是一段式描述方式&#xff0c;也就是将整个状态机全部写到一个always…

FPGA状态机(一段式、二段式、三段式)、摩尔型(Moore)和米勒型(Mealy)

1、状态机 1.1、理论 FPGA不同于CPU的一点特点就是CPU是顺序执行的&#xff0c;而FPGA是同步执行&#xff08;并行&#xff09;的。那么FPGA如何处理明显具有时间上先后顺序的事件呢&#xff1f;这个时候我们就需要使用到状态机了。 状态机简写为 FSM&#xff08;Finite Sta…

三段式状态机_verilog

这是一篇转载的文章&#xff0c;但是个人理解笔者将前两个always块结构搞错了&#xff0c;自行纠正了一下。 原文链接 https://www.jianshu.com/p/5eee434ab24d 1. 三段式要求 有限状态机采用三段式风格&#xff0c;即三个always块描述状态机. 第一个时序逻辑always块用来描…

2021-08-21Verilog三段式状态机的写法,标准示例和仿真。

Verilog三段式状态机的写法&#xff0c;标准示例和仿真。 第一段&#xff1a;同步状态转移。第一个always块格式化描述次态寄存器迁移到现态寄存器。 第二段&#xff1a;当前状态判断接下来的状态。组合逻辑always模块&#xff0c;描述状态转移条件判断用current_state 第三段&…

verilog 3段式状态机

3段式状态机&#xff1a; 3段式状态机写法&#xff0c;写出下图状态转换图。 1 确定输入输出信号&#xff0c;及其类型&#xff08;是wire还是reg&#xff09;&#xff1b; 2 声明内部信号&#xff0c;一般需要定义current_state和next_state&#xff1b; 3 用3个always语句描…

三段式状态机设计

状态机设计需满足的几个要求&#xff1a; 1. 三段式要求 有限状态机采用三段式风格&#xff0c;即三个always块描述状态机. 第一个组合逻辑always块用来描述下一状态的转移&#xff08;next state logic&#xff09; 第二个时序逻辑always块用来描述当前状态.&#xff08;cur…

三段式状态机浅析

三段式状态机 文章目录 三段式状态机三段式状态机优点书写格式formater整形器状态机书写举例 关注作者 三段式状态机优点 将组合逻辑与时序逻辑分开&#xff0c;所写代码层次清晰&#xff0c;方便理解和后续的维护相较两段式状态机&#xff0c;解决了输出毛刺的影响。 书写格…