Java内存模型

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理 硬件时提到的主内存名字一样,两者也可以类比,但物理上它仅是虚拟机内存的一部分)。每条线程 还有自己的工作内存(Working Memory,可与前面讲的处理器高速缓存类比),线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内 存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变 量,线程间变量值的传递均需要通过主内存来完成,
![[Pasted image 20230520212252.png]]

volatile关键字

特性

  • 保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。
  • 禁止指令重排序优化

并发下的问题

以下的代码,因为Java中运算符并非原子操作导致volatile变量在并发下发出现问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* volatile变量自增运算测试
*
* @author zzm
*/
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race++;
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
});
threads[i].start();
}
// 等待所有累加线程都结束
while (Thread.activeCount() > 1)
Thread.yield();
System.out.println(race);
}
}

运行程序会发现运行结果是小于200000的,问题出在race++并不是一个原子操作。

使用的场景

可见性

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。
    如以下代码:
1
2
3
4
5
6
7
8
9
volatile boolean shutdownRequested;  
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// 代码的业务逻辑
}
}

禁止指令重排

以下是我们常用的双锁检测单例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
Singleton.getInstance();
}
}

在多线程环境中,如果没有使用volatile关键字修饰instance变量,就可能出现双重检查锁定失效的情况。具体来说,由于指令重排的存在,一个线程可能会在第一次检查instance是否为null时得到false,但在执行完同步块之后,instance还没有被赋值,这时候如果有另一个线程访问getInstance()方法,就会再次创建新的实例,从而破坏单例模式。
使用volatile关键字可以禁止指令重排,保证instance变量的可见性和有序性,从而使双重检查锁定生效。当一个线程修改了volatile变量的值,其他线程能够立即看到修改后的最新值,从而避免了上述问题。