synchronized 关键字如何保证变量的可见性?请说明实现机制。
参考回答
synchronized
关键字通过内存屏障(Memory Barriers)来保证变量的可见性。当一个线程执行进入 synchronized
块时,它会强制刷新所有之前操作过的变量到主内存中,并且在退出 synchronized
块时,会将变量的最新值刷新到主内存,这样其他线程就可以看到最新的变量值。
实现机制:
- 加锁和解锁:当一个线程获得锁时,它会从主内存中读取共享变量的最新值;当它释放锁时,线程会将本地内存中的修改写回主内存。通过这种方式,
synchronized
保证了多个线程对共享变量的可见性。
详细讲解与拓展
1. synchronized
如何保证变量的可见性
synchronized
通过两种机制确保变量的可见性:
- 同步进入时的内存同步:
- 当一个线程进入一个
synchronized
块时,它会清空该线程的本地缓存,确保线程读取到主内存中的最新值。 - 通过获取锁,线程从主内存获取变量的最新值。
- 当一个线程进入一个
- 同步退出时的内存同步:
- 当线程退出
synchronized
块时,它会将所有的修改(本地内存中的变量值)写回主内存,确保其他线程能够看到这些最新值。
- 当线程退出
示例:
public class SynchronizedVisibilityExample {
private static int sharedVariable = 0;
public static synchronized void increment() {
sharedVariable++;
}
public static synchronized int getSharedVariable() {
return sharedVariable;
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
// 由于synchronized的保证,读取到的是最新的共享变量值
System.out.println(getSharedVariable()); // 输出 2000
}
}
解析:
- 线程1和线程2在执行
increment()
方法时,它们会竞争锁,但由于使用了synchronized
,每次执行时会有内存同步,确保线程间对sharedVariable
的更新是可见的。 - 通过
synchronized
确保了sharedVariable
在所有线程中保持一致,并且不会出现线程看不到对方更新的情况。
2. synchronized
内存模型中的 Happens-Before 关系
Java 内存模型(JMM)定义了 Happens-Before 关系,即当一个操作的发生顺序被指定为另一个操作的前置条件时,我们就可以确保前置操作的结果对后续操作是可见的。
对于 synchronized
关键字,它通过 Happens-Before 规则确保变量的可见性:
- 进入同步块时,所有线程对共享变量的更新都会被立即同步到主内存。
- 离开同步块时,所有对共享变量的修改都会被写回主内存,确保其他线程能看到最新的值。
这使得当一个线程进入同步块并修改了变量后,其他线程在获得锁时能够看到最新的变量状态。
3. synchronized
的可见性与原子性
- 可见性:
synchronized
保证了在进入和退出同步块时,变量的可见性,即确保线程间的内存同步,避免了线程缓存中看到旧值的情况。 - 原子性:
synchronized
也保证了同步块中的操作是原子的,即在一个线程执行同步块时,其他线程无法访问该同步块,避免了竞争条件。
然而,synchronized
并不能保证复合操作(如 count++
)的原子性。对于更复杂的操作,我们通常使用更精细的同步机制或原子类。
4. synchronized
如何与 CPU 缓存一致性机制结合工作
在多核处理器上,每个线程都有自己的局部缓存(如 CPU 缓存),因此,当线程修改共享变量时,它的修改可能只存在于线程的本地缓存中。为确保多个线程能够看到对共享变量的更新,Java 使用了内存屏障和缓存一致性协议。
- 内存屏障:通过在
synchronized
关键字执行的地方插入内存屏障(如LOCK
、UNLOCK
指令),确保所有线程都能够看到其他线程对共享变量的修改。 - 缓存一致性协议:CPU 会使用缓存一致性协议(如 MESI 协议)确保不同线程之间的内存同步,以确保每个线程看到的是共享变量的最新值。
5. volatile
和 synchronized
的比较
虽然 volatile
也能确保变量的可见性,但它与 synchronized
的实现机制不同:
volatile
:- 适用于单一变量的可见性,确保写入的值对其他线程可见,但不具备原子性,也不能解决复合操作问题。
volatile
是轻量级的同步机制,仅保证变量值的可见性,而不会阻止多个线程并发执行。
synchronized
:- 保证了变量的可见性,并且具有原子性,适合需要对多个变量或复杂操作进行同步的场景。
- 通过锁机制,阻塞其他线程,确保一个线程在执行临界区代码时其他线程无法进入。
6. 总结
synchronized
关键字通过内存屏障机制,确保了线程在进入同步块时,能够从主内存中读取到最新的共享变量值;并在退出同步块时,将线程本地的更新写回主内存,使其他线程能够看到最新的值。synchronized
保证了 可见性 和 原子性,并通过内存同步机制避免了线程间共享变量的可见性问题。- 与
volatile
不同,synchronized
除了保证可见性外,还能保证对临界区代码的原子操作。