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

Java并发编程: 探索synchronized的奥秘

synchronized 如何保证多线程的运行安全

synchronized 是 Java 内置的同步关键字,用于控制多线程对共享资源的访问。在并发编程中,它能确保线程安全,核心在于它保证了 原子性可见性,并通过 happens-before 关系在锁的获取和释放之间提供了有限的有序性。

synchronized 的三大特性

原子性(Atomicity)

  • 一个线程进入 synchronized 方法或代码块后,其他线程必须等待。
  • 这样能确保被锁保护的操作要么完整执行,要么不执行,不会被打断。

可见性(Visibility)

  • 当线程释放锁时,会把工作内存里的变量值刷新到主内存。
  • 当另一个线程获取锁时,会从主内存中读取最新值。
  • 保证了不同线程看到的共享变量状态是一致的。

有序性(Ordering)

  • synchronized 并不会禁止所有指令重排。
  • 但它保证了:锁的释放 happens-before 随后的同一锁的获取
  • 换句话说,线程 A 在释放锁前的操作,对线程 B 在获取锁后是可见的,并且不会被重排序到锁外。

因此,synchronized 的有序性是通过 内存屏障 + happens-before 规则间接保证的,而不是像 volatile 那样禁止指令重排。

synchronized 的原理

synchronized 的底层依赖于 对象头(Object Header)和 Monitor(管程)

  • 每个 Java 对象在内存中都有对象头,其中的 Mark Word 记录了锁状态(无锁、偏向锁、轻量级锁、重量级锁)。

  • 当线程进入 synchronized 代码块时,会执行加锁操作:

    • 通过 CAS 修改对象头,尝试将其标记为已锁
    • 如果获取失败,则进入自旋或阻塞,等待锁释放
  • 当线程退出同步块时,执行解锁操作,恢复对象头状态,并唤醒等待线程。

案例分析:共享计数器

public class SyncDemo {private int count = 0;public synchronized void increment() {count++;}public int getCount() {return count;}public static void main(String[] args) throws InterruptedException {SyncDemo demo = new SyncDemo();Runnable task = () -> {for (int i = 0; i < 1000; i++) {demo.increment();}};Thread t1 = new Thread(task);Thread t2 = new Thread(task);t1.start();t2.start();t1.join();t2.join();System.out.println(demo.getCount());}
}

上面代码是一个通过synchronized关键字修饰后,具备线程安全的代码。

如果没有 synchronizedcount++ 会分解成 3 步:

1.  读取 count
1.  执行加一
1.  写回主内存  多线程环境下可能交叉执行,导致结果小于 2000。

共享计数器未同步控制示例(数字演示)

这里取一次并发来阐述一下

假设初始 count = 0,两个线程各自执行一次 count++,具体交错情况如下:

时间点线程 1 操作线程 2 操作count 变化说明
t00初始值
t1读取 count = 00线程 1 读到 count
t2读取 count = 00线程 2 也读到 count
t3加一 → 10线程 1 本地加一,未写回
t4加一 → 10线程 2 本地加一,未写回
t5写回 11线程 1 写回 count = 1
t6写回 11线程 2 写回 count = 1,覆盖线程 1 的修改

最终结果:count = 1
理论上两个操作应该把 count 加到 2,但因为 交错读写导致丢失更新,最终只有 1。

字节码层面的实现

synchronized 在字节码中有 两种形式

修饰方法

public synchronized void increment() {count++;
}

字节码:

public synchronized void increment();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:0: aload_01: dup2: getfield      #2                  // Field count:I5: iconst_16: iadd7: putfield      #2                  // Field count:I10: return

方法被标记为 ACC_SYNCHRONIZED,JVM 会在调用时隐式执行加锁和解锁操作。

修饰代码块

public void add() {synchronized (this) {count++;}
}

字节码:

0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield      #2  // Field count:I
9: iconst_1
10: iadd
11: putfield      #2  // Field count:I
14: aload_1
15: monitorexit
16: goto          24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
  • monitorenter:获取锁
  • monitorexit:释放锁
  • 编译器生成异常处理块,保证异常退出时锁也被释放

对比总结

形式字节码实现方式适用范围
方法同步ACC_SYNCHRONIZED 方法标记,由 JVM 隐式加锁/解锁整个方法
代码块同步monitorenter / monitorexit方法内部的部分逻辑

ObjectMonitor 机制

在这里插入图片描述

您可以将 ObjectMonitor 想象成一个 守卫关键资源的城堡,它包含三个核心区域来管理进出的线程。

核心结构与状态变量

组件名称类型作用描述
OwnerThread线程引用当前持有锁的线程。 只要这个字段非空,就表示该 Monitor 处于被占用状态。
Entry Countint记录锁的 重入次数 (Re-entry count)。只有当这个计数器减到 0 时,锁才会被完全释放。
Mutex操作系统互斥量在锁升级到重量级锁后,Monitor 依赖它来实现线程的阻塞和唤醒。

线程的等待区域(三条队列)

线程根据其状态,会被 ObjectMonitor 放入不同的队列中。

EntryList / ContentionList(入口队列/竞争队列)
  • 功能: 存放所有等待竞争锁的线程。
  • 线程来源: 线程首次尝试进入 synchronized 块,但发现锁已被其他线程持有,就会被放入此队列等待。
  • 状态: 线程处于阻塞或自旋等待中。
WaitSet / WaitQueue(等待队列)
  • 功能: 存放调用了 wait() 方法 的线程。
  • 线程特征: 线程进入此队列前,已经释放了锁(Monitor)。
  • 状态: 线程处于等待状态,只有被 notify()notifyAll() 唤醒后,才会移动到 EntryList 去重新竞争锁。
OnDeck 线程(备选线程)
  • 功能: 在某些 HotSpot 实现中,用于存放一个 即将被唤醒 的线程,作为下一个获取锁的候选者。

锁的生命周期流程

  1. 竞争锁: 线程 → 尝试获取 Monitor。
  2. 获取成功: 线程 → 成为 OwnerThread,Entry Count +1。
  3. 获取失败: 线程 → 被放入 EntryList 阻塞等待。
  4. 主动等待: OwnerThread 在同步块中调用 wait() → 释放锁,Entry Count 清零 → 线程移动到 WaitSet 阻塞。
  5. 被动唤醒: 其他线程调用 notify() → WaitSet 中的线程 → 移动到 EntryList → 等待重新竞争锁。

锁升级过程

在这里插入图片描述

Java 6 开始,synchronized 引入 锁升级机制 来优化性能,包括:

锁状态描述获取锁过程
无锁状态对象未被任何线程持有锁无竞争,线程直接执行,无需加锁操作
偏向锁假设绝大多数情况下锁只被单个线程持有对象头记录偏向线程ID,线程访问无需同步开销
轻量级锁偏向锁撤销或多个线程交替执行同步代码块通过 CAS 修改对象头实现自旋锁,避免挂起线程
重量级锁竞争激烈,轻量级锁自旋失败JVM 使用底层 OS 的互斥量(Mutex)阻塞线程

无锁状态:

对象刚开始时处于无锁状态,也就是没有任何线程持有该对象的锁。

偏向锁:

为了减少无竞争情况下的锁开销,JVM引入了偏向锁。当一个线程首次访问同步代码块时,它会在对象头和当前线程的栈帧中记录偏向的线程ID。这样,在后续的执行中,如果仍然是同一个线程访问该同步代码块,JVM就可以判断出来,并允许该线程无锁地执行同步代码。偏向锁实际上是一种延迟加锁的机制,它的目标是消除无竞争情况下的同步原语,进一步提高程序的运行性能。

但是,当有其他线程尝试获取这个偏向锁时,偏向锁就会撤销,并尝试升级为轻量级锁。

在 JDK 18 中,偏向锁被标记为废弃的原因主要是基于实际的使用情况和性能分析。偏向锁的设计初衷是在无竞争或低竞争的情况下提高性能,通过减少不必要的锁操作来降低开销。然而,在实际应用中,JVM 开发者发现偏向锁并不总是能够提供预期的性能提升,有时甚至会成为性能瓶颈。

以下是一些导致偏向锁被废弃的关键因素:

  1. 复杂性:偏向锁的实现相对复杂,需要维护额外的锁状态和线程信息。这种复杂性不仅增加了开发和维护的成本,还可能引入潜在的错误和性能问题。
  2. 适用性有限:偏向锁主要适用于长时间持有锁且竞争不激烈的场景。然而,在实际应用中,很多锁的使用模式并不符合这个假设。如果锁被频繁地获取和释放,或者存在高度的竞争,偏向锁的优势就会大打折扣。
  3. 性能开销:尽管偏向锁的设计初衷是为了提高性能,但在某些情况下,它可能会导致额外的性能开销。例如,当偏向锁被撤销并升级为轻量级锁或重量级锁时,需要进行额外的锁状态转换和线程调度操作,这些操作可能会消耗大量的CPU资源。
  4. 其他同步原语的改进:随着Java并发包的不断演进,出现了更多更高效的同步原语,如 ReentrantLock、StampedLock 等。这些同步原语提供了更细粒度的锁控制,能够更好地适应不同的并发场景,因此在某些情况下可能更优于偏向锁。

轻量级锁:

轻量级锁是为了减少线程阻塞而设计的。当偏向锁撤销后,或者多个线程交替执行同步代码块时,锁会升级为轻量级锁。轻量级锁的加锁过程是通过CAS操作实现的,它试图将对象头的Mark Word替换为指向线程栈帧中锁记录的指针。如果成功,则当前线程获得锁;如果失败,说明存在竞争,此时会尝试自旋等待,即让当前线程空转一段时间,然后再次尝试获取锁。

如果自旋等待达到一定的次数仍然没有获取到锁,那么轻量级锁就会升级为重量级锁。

重量级锁

重量级锁是Java中最基础的锁机制,它的实现依赖于操作系统的互斥量(Mutex)。当轻量级锁无法满足性能需求时,会升级为重量级锁。此时,未获取到锁的线程会被阻塞,并进入等待状态,直到持有锁的线程释放锁。由于重量级锁涉及到用户态和内核态的切换,因此它的性能开销相对较大。

重量级锁的实现依赖于底层的 Monitor 机制。每个对象都有一个与之关联的 Monitor,当线程尝试获取重量级锁时,会被放入 Monitor 的入口等待队列中。如果获取锁失败,线程会被阻塞并放入等待队列,直到持有锁的线程释放锁。

偏向锁到重量级锁

假设对象初始为无锁状态,线程 T1、T2 交替执行同步方法:

  1. 无锁 → 偏向锁:T1 首次访问同步块,Mark Word 记录 T1 线程ID
  2. 偏向锁 → 轻量级锁:T2 尝试访问偏向锁,被撤销,两个线程自旋竞争
  3. 轻量级锁 → 重量级锁:自旋达到阈值仍未获取锁,升级为重量级锁,阻塞线程,直到锁释放

锁升级机制能在低竞争场景下消除同步开销,高竞争场景保证线程安全。

总结

  • synchronized 通过 对象头 + Monitor 实现锁机制
  • 保证了 原子性可见性,并通过 happens-before 提供了有限的有序性
  • 字节码层面表现为:方法同步 (ACC_SYNCHRONIZED) 和代码块同步 (monitorenter/monitorexit)
  • 数字示例验证了 未加锁时多线程会丢失更新,加锁后 synchronized 可以保证线程安全
  • synchronized关键字的最大好处是非常简单易用,但是也有很多局限性,比如对中断,加锁超时,多条件变量等支持不是很好,它仅仅支持非公平锁,很多时候,我们需要lock来做更加丰富的操作。
http://www.dtcms.com/a/420885.html

相关文章:

  • 网站流量太高 如何做负载均衡wordpress英文版登陆
  • C/C++ 指针详解与各种指针定义
  • 物流企业网站建设规划书王野天个人简历
  • 重庆平台网站建设工作建站网站建设
  • 网站管家wordpress 菜单状态
  • 网站开发教程wordpress 分类目录下不显示文章
  • 网站建设最好的公司排名WordPress添加百度联盟
  • 网站建站免费空间wordpress 前端个人中心 ajax 订单 支付宝
  • 深圳设计网站公司网站wordpress foxplayer
  • 创建自己的网站要钱吗广西建设厅网站招 标 信 息
  • wordpress会员vip插件昆山网站排名优化
  • 网站建设大企业房地产企业网站开发
  • Linux进程 --- 2
  • 柳市网站设计推广网站怎么做微信接口
  • 注册自己的网站需要多少钱网站 备案查询
  • 网站界面设计策划书怎么做招远网站建设公司
  • 网站规划的原则北京建设教育协会
  • 班级网站建设的系统概述合肥建站
  • 软考 系统架构设计师系列知识点之杂项集萃(159)
  • 外国ps修图网站开发网站流程
  • 做最好的网站新新常州企业自助建站系统
  • 天津市住房和城乡建设局网站派代网
  • 大连营商环境建设局网站代理记账如何获取客户
  • 网站开发什么技术拼团网站建设
  • 朝阳网站建设公司电话上海知名装修公司排名榜
  • 中华住房和城乡建设局网站网站网页不对称
  • 广州网站优化公司金融投资网站源码
  • 亚马逊卖家可以做促销的网站网站开发实战作业答案
  • wordpress 导航站主题网站建设代理推广徽信xiala5效果好
  • 查排名的网站WordPress显示插件