背景:线程安全的讨论
- CPU多核缓存架构
CPU缓存为了提高程序运行的性能,现代CPU在很多方面会对程序进行优化。CPU的处理速度是很快的,
内存的 速度次之,硬盘速度最慢。在CPU处理内存数据中,内存运行速度太慢,就会拖累CPU的速度。
为了解决这样的问题,CPU设计了多级缓存策略。
CPU分为三级缓存: 每个CPU都有L1,L2缓存,但是L3缓存是多核公用的。
CPU查找数据的顺序为: CPU -> L1 -> L2 -> L3 -> 内存 -> 硬盘
从CPU到 | 各级间大致时间 |
---|---|
主存 | 60~80纳秒 |
L3 | 大约15纳秒 |
L2 | 大约3纳秒 |
L1 | 大约1纳秒 |
寄存器 | 大约0.3纳秒 |
这种多级缓存的结构下,会有什么问题呢?
1.最经典的就是可见性的问题,可以简单的理解为,一个线程修改的值对其他线程可能不可见。
【最终可能导致数据不一致等问题】
2.除了存在可见性的问题,当多个线程同时修改相同资源的时候,还会存在资源的争夺问题(线程争抢)
【最终可能导致数据不一致,死锁等问题】
3.为了使处理器内部的运算单元尽量被充分利用。处理器可能会对输入的代码进行【乱序执行】
处理器会在计算之后将乱序执行的结果【进行重组】,保证该结果与顺序执行的结果是一致的,
但并不保证程序中各个语句的先后执行顺序与输入代码中的顺序一致
这就是指令重排
【最终可能导致数据不一致,程序逻辑错误等问题】
- (Java Memory Model)JMM—Java内存模型
屏蔽各种硬件和操作系统的内存访问之间的差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果
在java内存模型当中一样会存在可见性和指令重排的问题
volatile 示例详解
背景:在多线程环境下,线程 A 对共享变量进行修改后,线程 B 很可能看不到这个修改,原因是每个线程都有自己的工作内存,线程之间的数据不是直接共享的。当线程从主内存读取变量时,会将变量的副本存储在自己的工作内存中,而不是直接在主内存中操作。
volatile 是 Java 中的关键字,主要用于保证变量的可见性、禁止指令重排序优化。它主要解决了多线程并发访问共享变量时可能出现的问题。
1.可见性: 当一个变量被 volatile 修饰时,它保证了每个线程读取该变量的时候都是从主内存中读取的,而不是从线程自己的工作内存。
2.禁止指令重排序: volatile 关键字还具有禁止指令重排序的特性,保证了代码的执行顺序与写入的顺序一致。
package com.jdw.java8.thread;
/**
* volatile
*
* @author 唧唧复唧唧
* @Description volatile 解决线程数据可见性问题和指令重排
*/
public class VolatileDemo {
private static boolean fail = false;
private static volatile boolean success = false;
public static void main(String[] args) throws InterruptedException {
Thread threadFail = new Thread(new Runnable() {
@Override
public void run() {
while (!fail) {
}
System.out.println("子线程threadFail的while将会无限循环下去!");
}
});
Thread threadSuccess = new Thread(new Runnable() {
@Override
public void run() {
while (!success) {
}
System.out.println("因为有volatile关键字,子线程threadSuccess执行完成了!");
}
});
threadFail.start();
threadSuccess.start();
// 休眠2秒让子线程有足够的时间执行
Thread.sleep(3000);
// 修改退出子线程循环(因为线程间数据可见性的问题,这里修改了fail和success后,线程只有threadSuccess执行完成了)
fail = true;
success = true;
}
}
在Java语言中我们可以使用volatile关键字来保证一个变量在一次读写操作时的避免指令重排,volatile【内存屏障】是在我们的读写操作之前加入一条指令,当CPU碰到这条指令后必须等到前边的指令执行完成才能继续执行下一条指令。
在单线程环境中,如果向某个变量写入某个值,在没有其他写入操作的影响下,那么你总能取到你写入的那个值。然而在多线程环境中,当你的读操作和写操作在不同的线程中执行时,情况就并非你想象的理所当然,也就是说不满足多线程之间的可见性,所以为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
threadFail 线程一直在高速缓存中读取 fail 的值,不能感知主线程已经修改了 fail 的值而退出循环,这就是可见性的问题,使用【volatile】关键字可以解决这个问题
注意事项
-
volatile 适用于标识位的简单读写场景,对于复合操作,如递增等,建议使用 synchronized 或者 java.util.concurrent 包下的原子类
-
volatile 不能保证原子性。如果多个线程同时访问一个变量,并且其中有一个线程执行了写操作,volatile 不能保证其他线程在读取该变量时能够得到一个一致的值
-
volatile 不适用于替代 synchronized 的所有场景,只能确保可见性,无法保证原子性和互斥性
-
volatile 主要用于保证可见性和禁止指令重排序,适用于一些特定场景,但在一些复杂的场景中,需要结合其他机制来确保线程安全
-
volatile 关键字能保证数据的可见性,但不能保证数据的原子性,synchronized 关键字两者都能保证