赞
踩
volitate是Java虚拟机提供的轻量级同步机制关键字,但是无法保证线程安全
注意三点:保证可见性、不保证原子性、禁止进行指令重排序。
保证可见性
线程有工作内存,在操作一个变量的时候,会先去主内存拷贝这个变量到自己的工作内存,也就是副本。因为各自保存的是主内存的一个副本,那么当多线程修改时,就会出现错误现象。如图所示

而使用volitate关键字可以使它修饰的变量的读写操作都必须在内存中进行,这样某个线程更改了这个变量,其他线程读时会去读主内存,前一个线程的修改对当前线程是可见的。
例如,我们使用一个静态变量来控制一个线程的中断,开一个子线程,运行1s后让其终止。
public class VolitateTest { public static boolean setThreadStop = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (!setThreadStop) { } System.out.println("线程结束"); }).start(); Thread.sleep(1000); setThreadStop = true; System.out.println("已设置 setThreadStop = true"); } }
可以发现,尽管设置了控制线程终端的boolean为true,该线程仍不会终止,这就是因为,开子线程时,拷贝到子线程工作内存的是一个副本。

而如果加上volitate关键字,那么对该变量的修改就是强制在主内存中进行的。

可以看到,线程立马结束。

不保证原子性
原子性跟原子操作有关,比如我们常用的i++,它就不是一个原子操作,我们可以反编译一下看看

虽然add方法只有一行代码,但是我们可以反编译查看字节码。

我们使用代码进行测试,也就是多线程进行原子性操作,修改的静态变量使用volitate关键字进行修饰。
public class VolitateTest2 { public volatile static int num = 0; public static void add() { num++; } public static void main(String[] args) { for (int i = 0; i < 20 ; i++) { new Thread(() -> { for (int j = 0; j <1000 ; j++) { add(); } }).start(); } while (Thread.activeCount()>2) { Thread.yield(); } System.out.println(num); } }

出现了错误,由此可见volitate不可保证原子性。
当线程执行这个语句时,会先从内存当中读取 i 的值,并复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
问题出在这里:如果线程A和线程B同时操作 i,A读完、改完还没来得及改到内存中的时候,B读了一下i,这时候A改完了刷到内存了,B读到的就不是真实的值了。
于是有了缓存一致性协议,最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。
它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
禁止进行指令重排序
volitate关键字修饰的变量,在编译成汇编代码后,与该变量相关的一些指令,都会加上lock前缀。
什么叫指令重排序呢
指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序.
那么理论上会存在重新排序导致的错误的,而volitate则禁止指令重排序,通过内存屏障实现。
内存屏障是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。
内存屏障阻碍了CPU采用优化技术来降低内存操作延迟,必须考虑因此带来的性能损失。
Store1;
Store2;
Load1;
StoreLoad; //内存屏障
Store3;
Load2;
Load3;
对于上面的一组 CPU 指令(Store 表示写入指令,Load 表示读取指令),StoreLoad 屏障之前的 Store 指令无法与 StoreLoad 屏障之后的 Load 指令进行交换位置,即重排序。
但是 StoreLoad 屏障之前和之后的相同指令是可以互换位置的,即 Store1 可以和 Store2 互换,Load2 可以和 Load3 互换。
常见有 4 种屏障
Java 中对内存屏障的使用在一般的代码中不太容易见到,常见的有 volatile 和 synchronized 关键字修饰的代码块(后面再展开介绍),还可以通过 Unsafe 这个类来使用内存屏障。
JMM 为程序中所有的操作定义了一个偏序关系,称之为 先行发生原则(Happens-Before),简单来说,就是若满足先行发生原则,则代码执行是满足有序性的。
Happens-Before 是指 前面一个操作的结果对后续操作是可见的。
i = 1; //线程A执行
j = i ; //线程B执行
j 是否等于1呢?假定线程A的操作(i = 1)是happens-before 线程B的操作(j = i),那么可以确定线程B执行后j = 1 一定成立,如果他们不存在happens-before原则,那么j = 1 不一定成立。
这就是happens-before原则的威力。Happens-Before 非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作间是否可能存在冲突的所有问题。
B站狂神说
https://space.bilibili.com/95256449
https://dunwu.github.io/javacore/concurrent/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.html#_4-%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。