Java多线程与高并发三(volatile关键字)

mac2024-10-30  10

volatile关键字,开发中一般不用,主要用途就是用来面试造火箭,那么它在代码中的真正作用是啥呢??

上一篇博客说到,对于现代多核心计算机而言,在多线程的情况下,如果一个变量没有加volatile修饰,可能出现线程间各自拷贝主内存的变量值到自己独有的线程CPU缓存中对数据做各种操作,导致变量的变化在线程间不可见的问题。

volatile作用一:保证线程之间变量的可见性

举个例子:

public class VolatileTest1 { private Integer i = 0; /** * 改变i的值 */ public void f1() { int localVal = i; while (i < 5) { i = localVal++; System.out.println("改变i的值:" + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 监听i的值 */ public void f2() { int localVal = i; while (i < 5) { if (localVal != i) { System.out.println("i发生改变:" + i); localVal = i; } } } public static void main(String[] args) { VolatileTest1 v1 = new VolatileTest1(); new Thread(v1::f1).start(); new Thread(v1::f2).start(); } }

上述代码运行结果是:

运行多次,均是阻塞结尾。但是如若我们给变量i加上volatile过后:

public class VolatileTest1 { private volatile Integer i = 0; /** * 改变i的值 */ public void f1() { int localVal = i; while (i < 5) { i = localVal++; System.out.println("改变i的值:" + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 监听i的值 */ public void f2() { int localVal = i; while (i < 5) { if (localVal != i) { System.out.println("i发生改变:" + i); localVal = i; } } } public static void main(String[] args) { VolatileTest1 v1 = new VolatileTest1(); new Thread(v1::f1).start(); new Thread(v1::f2).start(); } }

变量i加上volatile后,方法f2的线程就能感知到i的变化,从而成功输出变化值。

其实volatile能让线程之间可见的原理就是,线程各自在CPU缓存的改变,volatile都强制让其刷新到主内存。

volatile作用二:禁止指令重排序

计算机CPU在执行程序指令的时候,出于性能考虑,只要指令最终语义与重排序之前一致,会把指令重新排序执行。

我们来看个简单的字节码

可以看到i++在字节码层面变成了

LINENUMBER 7 L0 GETSTATIC com/zab/concurrenttest/volatiletest/ByteCodeTest.i : I ICONST_1 IADD PUTSTATIC com/zab/concurrenttest/volatiletest/ByteCodeTest.i : I

步骤如下:

获取静态变量定义常量1i自增1把常量放回主内存

我们看看重排序的影响:

int a = 1; int b = 2; a++; b++;

假如重排序后:

int a = 1; a++; int b = 2; b++;

另一线程读取的时候,因为a++提前,可能把a=1,b=2的情况读成了a=2,b=2。

当然这只是描述重排序的后果,真正引起的问题的还是字节码指令级别的重排序。

我们已经知道指令重排序虽然可以提高执行效率,但是在并发条件下,却会出各种意想不到的问题。

在了解为什么volatile能够禁止指令重排序之前,我们需要了解什么是happen-before rules。

 

正如上图所说,如果代码块X不管怎么运行(单线程还是多线程)都在代码块Y前执行,那么X和Y之前具有一种happen-before的关系。这种关系在java中有如下几种:

1、线程的start()方法永远发生在其线程run()方法代码块执行之前

2、线程B的join()方法,保证线程B的run()代码块,在线程A插入join()方法后的代码块执行之前运行。

3、单线程语句靠前,执行优先

4、有锁的情况,释放锁发生在其余代码执行之前

5、加volatile的变量,多线程情况下,其变量写操作发生在读操作之前

happen-before规则盗图国外网站,感谢该网站把规则画得那么清晰明了。

https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/happens-before.html

第5种规则就是volatile所实现的,要达到这个目的,那又是通过什么技术手段呢?

手段就是内存屏障,内存屏障是由硬件支持的,不同的硬件平台实现内存屏障的手段不尽相同,java虚拟机屏蔽了硬件差异,统一生成内存屏障的指令。

硬件级的内存屏障分为Load Barrier和Store Barrier,说的简单点就是读、写屏障。

内存屏障的作用:

屏障前后的指令不能越过屏障重新排序,意味着屏障前的指令先执行,屏障后的指令后执行。强制刷新CPU缓存的数据至主内存。

内存屏障的用法:

读指令前加Load Barrier,可以让缓存失效,读到主内存最新的数据写指令后加Store Barrier,可以穿透缓存,写到主内存去

JVM虚拟机给我们提供四种内存屏障,是上面的读、写屏障的组合。

LoadLoad屏障:

用法:GETSTATIC X1; LoadLoad; GETSTATIC X2

在读取指令GETSTATIC X2要读取的数据被访问前,保证GETSTATIC X1要读取的数据被读取完毕,并且读取的是主内存数据。

StoreStore屏障:

用法:PUTSTATIC X1; StoreStore; PUTSTATIC X2

在写入指令PUTSTATIC X2写入执行前,保证PUTSTATIC X1的写入到主内存。

LoadStore屏障:

用法:GETSTATIC X1; LoadStore; PUTSTATIC X2

在写入指令PUTSTATIC X2写入前,保证GETSTATIC X1要读取的数据被读取完毕。

StoreLoad屏障:

用法:PUTSTATIC X1; StoreLoad; GETSTATIC X2

在读取指令GETSTATIC X2读取操作执行前,保证PUTSTATIC X1的写入对到主内存。

我们前面提到了happen-before规则还有内存屏障,这里总结下,规则是目的,屏障是手段!

总结volatile

1、volatile作用:保证线程可见性,禁止指令重排序。

2、volatile实现其作用的手段:内存屏障

最新回复(0)