线程安全的前提
线程安全,主要说的就是这几个方面:原子性,可见性,有序性。首先介绍一下这几个概念。
原子性:是指一个操作或是一组操作要么全部成功,要么全部失败。有点同生共死的感觉。多线程中一组操作可能会被打断,也就破坏了原子性。
可见性:是指一个线程的操作可以对另外一个线程可见。这里主要说的就是共享变量的读取会不会存在脏读的情况。
有序性:是指程序执行的顺序会按照编写代码的顺序。这里很多人会疑惑,什么鬼?难道不是吗?还真不一定,还记得单例模式中的双检索吗,会发生指令重排导致多线程环境下不安全。JVM 会自动优化代码执行顺序,当然最终结果不会变的,对于单线程来说。
所以说,在多线程的环境下,需要保证原子性,可见性,有序性。三者满足,即可开启线程安全之旅。那么回到今天的主题,volatile
有什么本领,它可以让共享变量安全在多线程下安全嘛?
先说结论,volatile
可以保证可见性和有序性,但是不能保证原子性。所以使用 volatile 修饰的共享变量在多线程的环境下,未必安全。
volatile 如何保证可见性
先看一段伪代码:这里 main 中定义一个 stop 变量,赋值为 true,线程 1 根据 stop 循环计数,线程 2 睡一会设置 stop 为 false ,线程 3 多睡一会,打印出 stop 变量。会发现打印出了 false 但是 while 循环还是没有停止。为啥呢?
main 方法
stop = true
thread1
int count;
while(stop) {
count++;
}
thread2
Thread.sleep(100);
stop = false;
thread3
Thread.sleep(200);
log.info(stop); // false
经常会看到使用主内存和线程的工作内存模型来解释这个,又看到另外一种解释,可能是更正确的解释,大家一起讨论一下。
JIT (just-in-time) Java 有一个即时编译器,它的主要功能就是对热点代码进行优化,像 for 循环, while 循环这种代码块,当循环次数大于某个值的时候,执行过于频繁,JVM 就会认为这块代码是热点代码。为了提高热点代码的执行效率,在运行时虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler)。
因为 JIT 的优化,thread 2 中的 while 循环中的 stop 会直接被认为是 true, 尽管主存中已经被修改为 false 了,但是while 还是没有停止。因为 thread 2 中的机器码没有变。
那怎么解决这种问题呢?JVM 给了一个选项,可以使用 -Xint 配置停止 JIT 优化,但是这样就会让整个项目的 JIT 都没有了,更好的方法就是使用 volatile 修饰 stop 参数,这样就是告诉虚拟机,不要使用 JIT 优化 stop。也正是因为这个特性,所以 volatile 可以保证可见性。
volatile 如何保证的有序性
volatile 可以通过添加内存屏障来保证有序性。这里需要先介绍另外一个东西,叫 happens-before 原则,原则有 8 条,记住是不可能的,举其中的一个例子感受一下,一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作,这种看起来不是很正常的嘛,原理就是 JVM 在后面默默付出呀,但是!JVM 为了优化代码,它还会进行指令重排!一行代码可能不单单只有一个指令呀,就是这个指令重排让原本有序的代码变成了无序的指令,所以可能会出现安全问题。
而 volatile 就可以通过添加内存屏障来保证指令的安全有序,具体来说就是它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到 volatile 关键字的时候,在它前面的操作已经全部完成;同时它又是拒绝 JIT 的,所有的改动会同时被所有的线程看到。
volatile 不保证原子性
说一个小小的例子就行了,volatile 只能修饰一个变量呀,对这这个变量可以进行的操作本身可以不是原子性的呀。
public volatile int count = 0;
count ++; // ++ 操作本身不是原子性的
synchronized 怎么就保证安全了呢
synchronized 的代码块同一时间,只能有一个线程可以进入执行,也就满足了代码块整体的原子性,可见性,有序性。