synchronized 关键字如何保证变量的可见性?请说明实现机制。

参考回答

synchronized 关键字通过内存屏障(Memory Barriers)来保证变量的可见性。当一个线程执行进入 synchronized 块时,它会强制刷新所有之前操作过的变量到主内存中,并且在退出 synchronized 块时,会将变量的最新值刷新到主内存,这样其他线程就可以看到最新的变量值。

实现机制

  • 加锁和解锁:当一个线程获得锁时,它会从主内存中读取共享变量的最新值;当它释放锁时,线程会将本地内存中的修改写回主内存。通过这种方式,synchronized 保证了多个线程对共享变量的可见性。

详细讲解与拓展

1. synchronized 如何保证变量的可见性

synchronized 通过两种机制确保变量的可见性:

  1. 同步进入时的内存同步
    • 当一个线程进入一个 synchronized 块时,它会清空该线程的本地缓存,确保线程读取到主内存中的最新值。
    • 通过获取锁,线程从主内存获取变量的最新值。
  2. 同步退出时的内存同步
    • 当线程退出 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 使用了内存屏障和缓存一致性协议。

  1. 内存屏障:通过在 synchronized 关键字执行的地方插入内存屏障(如 LOCKUNLOCK 指令),确保所有线程都能够看到其他线程对共享变量的修改。
  2. 缓存一致性协议:CPU 会使用缓存一致性协议(如 MESI 协议)确保不同线程之间的内存同步,以确保每个线程看到的是共享变量的最新值。

5. volatilesynchronized 的比较

虽然 volatile 也能确保变量的可见性,但它与 synchronized 的实现机制不同:

  • volatile
    • 适用于单一变量的可见性,确保写入的值对其他线程可见,但不具备原子性,也不能解决复合操作问题。
    • volatile 是轻量级的同步机制,仅保证变量值的可见性,而不会阻止多个线程并发执行。
  • synchronized
    • 保证了变量的可见性,并且具有原子性,适合需要对多个变量或复杂操作进行同步的场景。
    • 通过锁机制,阻塞其他线程,确保一个线程在执行临界区代码时其他线程无法进入。

6. 总结

  • synchronized 关键字通过内存屏障机制,确保了线程在进入同步块时,能够从主内存中读取到最新的共享变量值;并在退出同步块时,将线程本地的更新写回主内存,使其他线程能够看到最新的值。
  • synchronized 保证了 可见性原子性,并通过内存同步机制避免了线程间共享变量的可见性问题。
  • volatile 不同,synchronized 除了保证可见性外,还能保证对临界区代码的原子操作。

发表评论

后才能评论