多线程剖析之内存模型、原子性和可见性
2023-12-06 19:45:15
前言
在多线程编程中,我们经常会遇到一些看似奇怪的现象,比如:
- 多个线程同时对一个共享变量进行修改,导致数据不一致。
- 一个线程修改了共享变量,但另一个线程却看不到这个修改。
要理解这些现象产生的原因,就必须理解 Java 线程内存模型。
Java 线程内存模型
Java 线程内存模型(Java Memory Model,JMM)定义了线程如何访问共享变量的规则。JMM 规定,每个线程都有自己的私有内存,称为线程私有内存(Thread-Local Memory,TLM)。当一个线程修改一个共享变量时,这个修改只会发生在该线程的 TLM 中。其他线程无法直接访问该线程的 TLM,因此它们看不到这个修改。
为了让其他线程能够看到这个修改,需要将这个修改从 TLM 复制到主内存(Main Memory,MM)中。主内存是所有线程共享的内存区域,因此其他线程可以从主内存中读取这个修改。
JMM 还定义了两种类型的变量:
- 共享变量: 可以在多个线程中访问的变量。
- 局部变量: 只能在声明它的线程中访问的变量。
局部变量存储在 TLM 中,共享变量存储在主内存中。
原子性
原子性(Atomicity)是指一个操作要么完全执行,要么完全不执行,不存在中间状态。在 Java 中,原子性由原子变量(Atomic Variable)来保证。原子变量是通过特殊的硬件指令来实现的,这些指令可以确保一个操作在执行过程中不会被其他线程中断。
Java 中提供了原子变量类,例如 AtomicInteger、AtomicLong 等。这些类提供了原子性的增减、比较和交换操作。
可见性
可见性(Visibility)是指一个线程修改了共享变量,其他线程能够看到这个修改。在 Java 中,可见性由 volatile 来保证。volatile 关键字可以确保一个变量在被修改后立即被写入主内存,并且其他线程可以立即从主内存中读取这个修改。
volatile 关键字还可以防止指令重排序。指令重排序是指编译器和处理器可以改变指令的执行顺序,以提高程序的性能。但是,指令重排序可能会导致共享变量的修改对其他线程不可见。volatile 关键字可以防止指令重排序,从而确保共享变量的修改对其他线程始终可见。
举个例子
为了更好地理解 Java 线程内存模型、原子性和可见性,我们来看一个例子。
public class SharedCounter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
这个类定义了一个共享变量 count,以及两个方法 increment() 和 getCount()。increment() 方法对 count 进行递增操作,getCount() 方法返回 count 的值。
现在,我们创建两个线程,每个线程都调用 increment() 方法 10000 次,然后调用 getCount() 方法获取 count 的值。
public class Main {
public static void main(String[] args) {
SharedCounter counter = new SharedCounter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter.getCount());
}
}
运行这个程序,我们会发现 count 的值可能不是 20000,而是小于 20000。这是因为 increment() 方法和 getCount() 方法都不是原子的,因此可能出现一个线程修改了 count,但另一个线程却看不到这个修改的情况。
为了解决这个问题,我们可以使用原子变量来保证 increment() 方法和 getCount() 方法的原子性。
public class SharedCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
现在,我们再次运行这个程序,我们会发现 count 的值始终是 20000。这是因为 AtomicInteger 类提供了原子性的增减和获取操作,因此可以保证 increment() 方法和 getCount() 方法的原子性。
结论
Java 线程内存模型、原子性和可见性是多线程编程中非常重要的概念。理解这些概念可以帮助我们编写出更加健壮和安全的并发程序。