当前位置: 首页 > news >正文

Java 中 Synchronized如何保证可见性

在 Java 多线程编程中,可见性问题是指一个线程对共享变量的修改,其他线程能够立即看到。如果没有适当的同步机制,可能会出现线程 A 修改了共享变量的值,但线程 B 仍然使用的是修改前的值,导致程序出现错误。synchronized 是 Java 中的一种内置锁机制,它不仅可以保证操作的原子性,还能确保线程之间的可见性。

Synchronized 保证可见性的原理

  • 底层实现基础 :每个对象都有一个与之关联的锁。当一个线程获取了对象的锁后,其他线程必须等待该锁被释放才能进入相关的同步代码块或方法。
  • 获取锁时的操作 :当一个线程获取到锁时,会将共享变量从主内存拷贝到工作内存中,以便线程对这些变量进行读写操作。在获取锁的过程中,线程会清空工作内存中相关变量的值,确保之后使用变量时,必须从主内存中重新加载最新的值,而不是使用工作内存中可能过时的副本。
  • 释放锁时的操作 :当线程执行完同步代码块或方法并释放锁时,会将工作内存中修改后的共享变量的值刷新回主内存。这样,其他线程在获取到同一个锁后,可以从主内存中读取到最新的变量值,保证了线程之间共享变量的可见性。

具体示例分析

假设有一个共享变量 flag,初始值为 true。线程 A 和线程 B 都需要访问这个变量。在线程 A 中,将 flag 设置为 false,并且这个操作是在一个由 synchronized 修饰的同步代码块中进行的。在线程 B 中,有一个循环不断地判断 flag 的值,当 flagfalse 时退出循环。

以下是代码示例:

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 会自动在这段代码前后插入特殊的指令(monitorentermonitorexit)来管理和控制线程的执行。

以如下代码为例:

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();}
}

如果觉得有帮助请给个赞,点个关注,谢谢啦

http://www.xdnf.cn/news/1945.html

相关文章:

  • html+js+clickhouse环境搭建
  • Java项目——校园社交网络平台的设计与实现
  • 考研单词笔记 2025.04.17
  • 音视频学习 - ffmpeg 编译与调试
  • 【零基础】基于DeepSeek-R1与Qwen2.5Max的行业洞察自动化平台
  • 记录一次生产中mysql主备延迟问题处理
  • python学习—详解word邮件合并
  • Redis List 的详细介绍
  • 方德桌面操作系统V5.0-G23 vim无法复制粘贴内容
  • Java虚拟机(JVM)平台无关?相关?
  • 在Linux下安装Gitlab
  • 2.深入剖析 Rust+Axum 类型安全路由系统
  • 极狐GitLab GEO 功能介绍
  • DAY 47 leetcode 232--栈与队列.用栈实现队列
  • vue3 element-plus中的国际化在onMounted中的写法
  • docker Windows 存放位置
  • 【web考试系统的设计】
  • 零服务器免备案!用Gitee代理+GitHub Pages搭建个人博客:绕过443端口封锁实战记录
  • 基于Flask的漏洞挖掘知识库系统设计与实现
  • 对抗生成进化:基于DNA算法的AIGC检测绕过——让AI创作真正“隐形“
  • 生物信息学技能树(Bioinformatics)与学习路径
  • 04-libVLC的视频播放器:获取媒体信息
  • 【裁员感想】
  • 关于webpack的知识点
  • 《似锦》:画饼之—你画给我我画给你
  • java 设计模式之代理模式
  • Android Compose Activity 页面跳转动画详解
  • 【Leetcode 每日一题】2176. 统计数组中相等且可以被整除的数对
  • ubuntu磁盘挂载
  • MySQL GTID集合运算函数总结