在这篇文章Java并发—Java内存模型以及线程安全-CSDN博客多次提及volatile关键字,这是一个非常重要的概念,主要用于多线程编程中,它确保了变量的可见性和禁止指令重排序,但不保证原子性,下面详细解释volatile关键字的作用和特性:
1、volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证可见性
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
上篇文中有讲一个例子:🌰线程A和线程B从主内存读取和修改x=1的过程
由于线程A对变量的修改x=2未立即对线程B可见,造成了线程安全问题,为了确保线程间的即时通信和数据一致性,使用 volatile 关键字是必要的
如果在用volatile修饰变量x后,都会从主内存中读取最新的最新的值,而不是从线程自己的工作内存中读取可能过期的版本
线程A和线程B要进行通信的话,必须要进行以下几个步骤:
-
初始化:x = 1,存储在主内存。
-
线程A读取:A从主内存读取x,复制x=1到A的工作内存。
-
线程B读取:B从主内存读取x,复制x=1到B的工作内存。
-
线程A修改:A在工作内存中修改x=2
-
线程A写回:A将工作内存中的x=2写回主内存。
-
线程B重新读取:B从主内存读取最新的x=2,保证了数据的可见性。
这个过程展示了JMM如何确保多线程环境下的数据一致性
看起来这个流程没什么问题,但是线程A的修改和写回的操作不是原子性的,可能在线程A还未写回,线程B已经重新读取了,这个问题先按下不表
因此:volatile写-读的内存语义
-
当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量值刷新到主内存。
-
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程将从主内存中读取共享变量
2)禁止进行指令重排序
volatile关键字禁止指令重排序有两层意思:
-
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
-
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行
🌰例子:以下面的代码来讲述使用和未使用volatile关键字会出现什么杨的情况来理解指令重排
🔴未使用 volatile 关键字
class Example {
int a = 0;
boolean ready = false;
public void writerThread() {
a = 5; // 语句1
ready = true; // 语句2
}
public void readerThread() {
while (!ready) { // 语句3
Thread.yield();
}
System.out.println(a); // 语句4
}
}
编译器和处理器可能会为了优化性能而对指令进行重排序。在这种情况下,语句1和语句2之间没有数据依赖关系,所以它们可能被重排序。
例如:处理器可能会先执行语句2再执行语句1
可能的执行顺序
写线程:执行语句2,然后执行语句1。
读线程:执行语句3,检查 ready 是否为 true。如果 ready 已经被设置为 true(即使 a 还没有被设置为5),读线程将进入语句4并打印 a 的值,此时 a 可能还是默认的0。
结果
由于指令重排序,读线程可能在 a 被写线程修改之前就读取了 a 的值,导致输出结果为0,而不是预期的5。这就是指令重排序可能导致的线程安全问题
🔴使用 volatile 关键字
class Example {
volatile int a = 0;
volatile boolean ready = false;
public void writerThread() {
a = 5; // 语句1
ready = true; // 语句2
}
public void readerThread() {
while (!ready) { // 语句3
Thread.yield();
}
System.out.println(a); // 语句4
}
}
指令重排序限制
volatile 关键字会阻止编译器和处理器对与 volatile 变量相关的指令进行重排序。这意味着语句1和语句2之间的执行顺序将被保留,确保了写线程先修改 a 的值,然后再设置 ready 为 true
结果
由于 volatile 的内存屏障效果,读线程在检查 ready 是否为 true 并进入语句4之前,将看到写线程对 a 的最新修改,从而避免了指令重排序引起的线程安全问题
总结来说,volatile 关键字通过提供内存屏障来限制指令重排序,确保了变量的可见性和一定程度上的有序性,从而帮助解决多线程环境下的指令重排序问题
2、 volatile关键字的使用场景
-
适用场景:volatile适用于那些被多个线程访问但并不涉及复合操作(例如递增操作)的变量。典型的使用场景包括状态标志、控制变量等。
-
不适用场景:不要将volatile用于需要原子性操作的场景,因为volatile并不能保证原子性。对于需要原子性操作的场景,应该使用锁或者Atomic原子类
实际开发中,几乎看不到volatile的使用,因为volatile只能保证可见性,并不能保证原子性,就需要结合CAS(Compare and Swap)
其实在Java中,java.util.concurrent.atomic包提供了一组原子类,比如AtomicInteger、AtomicLong、AtomicBoolean等,它们提供了一种无锁的线程安全机制,以确保对变量的操作是原子性的。
当谈到Atomic原子类的实现原理时,CAS操作是其中的关键。CAS是一种乐观锁技术,它涉及比较内存中的值和预期值,如果相等,则使用新值替换内存中的值。在Java中,CAS是通过Unsafe
类实现的,它是一种硬件级别的原子性操作
但是,CAS操作本身无法解决线程可见性的问题,这就是volatile
关键字的作用。volatile
关键字可以确保变量的写操作立即可见于其他线程,从而解决了线程之间的可见性问题。因此,Atomic原子类是结合了CAS和volatile关键字来实现线程安全
一般情况下都是直接使用的Atomic原子类来保证线程安全的情况,并不会去直接使用volatile
关键字
在上面的例子中使用了volatile关键字修改共享变量x的过程,线程A的修改和写回的操作不是原子性的,那么CAS就可以解决这个问题,至于如何解决,在下篇文章再讲吧……
下一篇:Java并发—CAS的原理及应用场景-CSDN博客