Java 中 Synchronized如何保证可见性
在 Java 多线程编程中,可见性问题是指一个线程对共享变量的修改,其他线程能够立即看到。如果没有适当的同步机制,可能会出现线程 A 修改了共享变量的值,但线程 B 仍然使用的是修改前的值,导致程序出现错误。synchronized
是 Java 中的一种内置锁机制,它不仅可以保证操作的原子性,还能确保线程之间的可见性。
Synchronized 保证可见性的原理
- 底层实现基础 :每个对象都有一个与之关联的锁。当一个线程获取了对象的锁后,其他线程必须等待该锁被释放才能进入相关的同步代码块或方法。
- 获取锁时的操作 :当一个线程获取到锁时,会将共享变量从主内存拷贝到工作内存中,以便线程对这些变量进行读写操作。在获取锁的过程中,线程会清空工作内存中相关变量的值,确保之后使用变量时,必须从主内存中重新加载最新的值,而不是使用工作内存中可能过时的副本。
- 释放锁时的操作 :当线程执行完同步代码块或方法并释放锁时,会将工作内存中修改后的共享变量的值刷新回主内存。这样,其他线程在获取到同一个锁后,可以从主内存中读取到最新的变量值,保证了线程之间共享变量的可见性。
具体示例分析
假设有一个共享变量 flag
,初始值为 true
。线程 A 和线程 B 都需要访问这个变量。在线程 A 中,将 flag
设置为 false
,并且这个操作是在一个由 synchronized
修饰的同步代码块中进行的。在线程 B 中,有一个循环不断地判断 flag
的值,当 flag
为 false
时退出循环。
以下是代码示例:
public class SynchronizedVisibilityExample {private static boolean flag = true;public static void main(String[] args) {new Thread(() -> {synchronized (SynchronizedVisibilityExample.class) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}flag = false;System.out.println("线程 A 已将 flag 设置为 false");}}, "线程 A").start();new Thread(() -> {synchronized (SynchronizedVisibilityExample.class) {while (flag) {// 线程 B 在这里等待,直到 flag 被设置为 false}System.out.println("线程 B 检测到 flag 为 false,退出循环");}}, "线程 B").start();}
}
- 线程 A 执行过程 :线程 A 获取到锁后,将
flag
从主内存拷贝到工作内存,修改其值为false
。在释放锁之前,线程 A 会将工作内存中修改后的flag
值刷新回主内存。 - 线程 B 执行过程 :线程 B 在每次循环迭代时,都需要获取同一个锁。获取锁后,线程 B 会清空工作内存中
flag
的值,并从主内存中重新加载flag
的最新值。当线程 A 已经将flag
修改为false
并刷新到主内存后,线程 B 在后续获取锁并加载flag
值时,就会得到false
,从而退出循环。
从字节码层面分析 Synchronized 如何保证可见性
当我们用 synchronized
修饰方法或者代码块时,JVM 会自动在这段代码前后插入特殊的指令(monitorenter
,monitorexit
)来管理和控制线程的执行。
以如下代码为例:
public class SynchronizedExample {public void method() {synchronized (this) {System.out.println("Synchronized Method");}}
}
编译后的字节码如下:
public class SynchronizedExample {public void method();Code:0: aload_0 // 将"this"加载到操作数栈顶1: dup // 复制操作数栈顶的值2: astore_1 // 将操作数栈顶的值存储到局部变量表的第1个位置,也就是"this"3: monitorenter // 为对象(此处即"this")加锁4: getstatic #2 // 获取静态字段,此处是 java/lang/System.out:Ljava/io/PrintStream;7: ldc #3 // 从常量池中加载字符串常量 "Synchronized Method"9: invokevirtual #4 // 调用方法,此处是 java/io/PrintStream.println:(Ljava/lang/String;)V12: aload_1 // 把局部变量表的第1个位置的值(即"this")加载到操作数栈顶13: monitorexit // 为对象(此处即"this")解锁14: goto 2217: astore_2 // 将操作数栈顶的异常存储到局部变量表的第2个位置18: aload_1 // 把局部变量表的第1个位置的值(即"this")加载到操作数栈顶19: monitorexit // 为对象(此处即"this")解锁20: aload_2 // 把局部变量表的第2个位置的值(即异常)加载到操作数栈顶21: athrow // 抛出异常22: return // 方法返回Exception table: // 异常处理表from to target type4 14 17 any17 20 17 any
}
monitorenter
指令 :位于同步代码块的前端,表示当前线程尝试获取锁。如果获取成功,线程就可以执行同步代码块;如果失败,线程就会被阻塞,直到获取到锁。当线程获取锁后,会清空工作内存中相关变量的值,确保从主内存中重新加载最新的值。monitorexit
指令 :位于同步代码块的后端,表示当前线程释放锁。当线程释放锁时,会将工作内存中修改后的共享变量的值刷新回主内存,这样其他线程在获取到同一个锁后,可以从主内存中读取到最新的变量值,保证了线程之间共享变量的可见性。
与 Volatile 的对比
Volatile
变量也具有可见性,它通过在写操作后加入内存屏障,在读操作前加入内存屏障来实现。当一个线程写一个 volatile
变量时,会强制将修改后的值立即刷新到主内存;当其他线程读取这个 volatile
变量时,会强制从主内存中读取最新的值。而 synchronized
是通过获取锁和释放锁的过程来保证可见性的,两者的实现机制不同,但都解决了线程之间的可见性问题。
以下是使用 volatile
的代码示例:
public class VolatileVisibilityExample {private static volatile boolean flag = true;public static void main(String[] args) {new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}flag = false;System.out.println("线程 A 已将 flag 设置为 false");}, "线程 A").start();new Thread(() -> {while (flag) {// 线程 B 在这里等待,直到 flag 被设置为 false}System.out.println("线程 B 检测到 flag 为 false,退出循环");}, "线程 B").start();}
}
如果觉得有帮助请给个赞,点个关注,谢谢啦