Java线程中的原子性、有序性和可见性的理解
在《Java 并发编程实战》的3.13章节是这样描述的:内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。意思是:当两个线程之间通过“内置锁”(即 synchronized)进行同步时,线程 B 可以看到线程 A 之前对共享变量的修改结果,而且这些修改是按照预期的顺序可见的。
happens-before
会牵扯三个关键字:synchronized、volatile、final。
为什么不加锁就看不到对方的修改?
因为Java对变量的修改是保存在自己的工作内存中,如果没有同步机制,本地线程的变量其他线程是看不见的。
不加锁看不到修改的案例:在下面的案例中,main方法启动后,先启动ReaderThread的线程,然后休眠1秒,再修改ready的值,但是因为没有加锁和volatile关键字,导致休眠1秒之后,ready = true;这个操作在ReaderThread的run方法中一直无法看见。
public class NoVisibility2 {
private static boolean ready;
private static int number;
public static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
}
System.out.println(number);
}
}
public static void main(String[] args) throws InterruptedException {
new NoVisibility2.ReaderThread().start();
TimeUnit.SECONDS.sleep(1);
ready = true;
number = 42;
}
}
用volatile关键字来修饰,目的是为了线程之间的可见性。以下是案例:private static volatile boolean ready;这一句做了修改。结果会输入42.
public class NoVisibility2 {
private static volatile boolean ready;
private static int number;
public static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
}
System.out.println(number);
}
}
public static void main(String[] args) throws InterruptedException {
new NoVisibility2.ReaderThread().start();
TimeUnit.SECONDS.sleep(1);
ready = true;
number = 42;
}
}
加锁实现可见性和有序性
public class NoVisibility2 {
private static boolean ready;
private static int number;
private static synchronized boolean isReady(){
return ready;
}
private static synchronized void setReady(boolean flag){
ready = flag;
}
private static synchronized void setNumber(int num){
number = num;
}
private static synchronized int getNumber(){
return number;
}
public static class ReaderThread extends Thread {
@Override
public void run() {
while (!isReady()) {
}
System.out.println(getNumber());
}
}
public static void main(String[] args) throws InterruptedException {
new NoVisibility2.ReaderThread().start();
TimeUnit.SECONDS.sleep(1);
setReady(true);
setNumber(42);
}
}
在这个例子中,“锁”起到了两个作用:
| 作用 | 说明 |
|---|---|
| 可见性 | 线程 A 在释放锁前的写操作,对线程 B 是可见的,因为线程 B 在获取锁前,必须刷新缓存读取主内存 |
| 有序性 | 线程 A 在锁内的指令,线程 B 会按照同样顺序读取,不会出现指令重排序带来的错乱 |
| JMM 规定了: |
- 释放锁(unlock)之前,线程必须把对共享变量的修改刷新到主内存。
- 获取锁(lock)之后,线程必须清空自己的工作内存并从主内存重新读取变量。
只要线程 A 在锁内写,线程 B 在锁内读,它一定能看到 A 写的内容
volatile 与 synchronized 的比较图
| 特性 | volatile |
synchronized |
|---|---|---|
| 可见性 | ✅ 有 | ✅ 有 |
| 原子性(复合操作) | ❌ 无 | ✅ 有 |
| 是否可重入 | ❌ 不存在 | ✅ 是 |
| 阻塞性/性能影响 | ✅ 非阻塞(开销小) | ❌ 阻塞(性能相对差) |
| 指令重排序禁止 | ✅ 禁止重排序 | ✅ 禁止重排序 |
| 使用场景 | 状态标志、信号量等 | 临界区代码、复合状态更新 |
final:JVM对final有特殊的可见性保障,一旦构造器完成之后,其他线程就一定能看到正确的内容,不会出现默认值或者半初始化的状态。
class Holder {
int value;
public Holder() {
value = 42;
}
}
Holder holder;
public void writer() {
holder = new Holder(); // value = 42
}
public void reader() {
if (holder != null) {
System.out.println(holder.value); // ❌ 可能打印出 0(未初始化)
}
}
非final修饰成员变量的影响:在多线程环境下,由于构造器执行和赋值 holder 可能被重排序,导致读线程拿到一个 构造未完成的对象

浙公网安备 33010602011771号