侧边栏壁纸
博主头像
搞钱拒绝ICU

行动起来,活在当下

  • 累计撰写 26 篇文章
  • 累计创建 8 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

synchronized关键字

admin
2024-04-27 / 0 评论 / 0 点赞 / 19 阅读 / 0 字

一、介绍

JDK早期版本中

synchronized 属于 重量级锁,效率低下。这是因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex(互斥)Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

JDK6 之后

synchronized 进行了锁优化,增加了偏向锁、轻量级锁、锁消除等技术来减少锁操作的开销,这些优化让 synchronized 锁的效率提升了很多。因此, synchronized 还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 synchronized

具体优化:

①、偏向锁:同一个线程可以多次获取同一把锁,无需重复加锁。

②、轻量级锁:当没有线程竞争时,通过 CAS 自旋等待锁,避免直接进入阻塞。

③、锁消除:JIT 可以在运行时进行代码分析,如果发现某些锁操作不可能被多个线程同时访问,就会对这些锁进行消除,从而减少上锁开销。

JDK新版本

由于偏向锁增加了 JVM 的复杂性,同时也并没有为所有应用都带来性能提升。因此,在 JDK15 中,偏向锁被默认关闭(仍然可以使用 -XX:+UseBiasedLocking 启用偏向锁),在 JDK18 中,偏向锁已经被彻底废弃(无法通过命令行打开)

官方弃用公告:

JEP 374: Deprecate and Disable Biased Locking

在官方声明中,主要原因有两个方面:

  • 性能收益不明显:

偏向锁是 HotSpot 虚拟机的一项优化技术,可以提升单线程对同步代码块的访问性能。

受益于偏向锁的应用程序通常使用了早期的 Java 集合 API,例如 HashTable、Vector,在这些集合类中通过 synchronized 来控制同步,这样在单线程频繁访问时,通过偏向锁会减少同步开销。

随着 JDK 的发展,出现了 ConcurrentHashMap 高性能的集合类,在集合类内部进行了许多性能优化,此时偏向锁带来的性能收益就不明显了。

偏向锁仅仅在单线程访问同步代码块的场景中可以获得性能收益。

如果存在多线程竞争,就需要 撤销偏向锁 ,这个操作的性能开销是比较昂贵的。偏向锁的撤销需要等待进入到全局安全点(safe point),该状态下所有线程都是暂停的,此时去检查线程状态并进行偏向锁的撤销。

  • JVM 内部代码维护成本太高:

偏向锁将许多复杂代码引入到同步子系统,并且对其他的 HotSpot 组件也具有侵入性。这种复杂性为理解代码、系统重构带来了困难,因此, OpenJDK 官方希望禁用、废弃并删除偏向锁。

二、使用分类

synchronized 依赖 JVM 内部的 Monitor对象来实现线程同步。使用的时候不用手动去 lock 和 unlock,JVM 会自动加锁和解锁。

构造方法不能使用 synchronized 关键字修饰。不过,可以在构造方法内部使用 synchronized 代码块。构造方法本身是线程安全的

Monitor对象

Monitor 是 JVM 内置的同步机制,每个对象在内存中都有一个对象头——Mark Word,用于存储锁的状态,以及 Monitor 对象的指针。

博客园Zebt:Java 对象头

synchronized 依赖对象头的 Mark Word 进行状态管理,支持无锁、偏向锁、轻量级锁,以及重量级锁。

在 Hotspot 虚拟机中,Monitor 由 ObjectMonitor 实现:

+----------------------+
|  ObjectMonitor      |
|  ----------------   |
|  _owner = Thread-1  |  // 当前持有锁的线程
|  _count = 1         |  // 线程获取锁的次数
|  _WaitSet -> T3,T4  |  // 执行 wait() 的线程
|  _EntryList -> T2,T5|  // 竞争锁的线程
|  _cxq -> T6,T7      |  // 新进入的线程
+----------------------+

synchronized 加锁代码块时

JVM 会通过 monitorentermonitorexit 两个指令来实现同步:

  • 前者表示线程正在尝试获取 lock 对象的 Monitor;

  • 后者表示线程执行完了同步代码块,正在释放锁。

使用 javap -c -s -v -l SynchronizedDemo.class 反编译 synchronized 代码块时,就能看到这两个指令。

三分恶面渣逆袭:monitorenter和monitorexit

synchronized 修饰普通方法时

JVM 会通过 ACC_SYNCHRONIZED 标记符来实现同步。

三分恶面渣逆袭:synchronized修饰同步方法

三、锁升级

①、偏向锁:当一个线程第一次获取锁时,JVM 会在对象头的 Mark Word 记录这个线程 ID,下次进入 synchronized 时,如果还是同一个线程,可以直接执行,无需额外加锁。

②、轻量级锁:当多个线程尝试获取锁但不是同一个时段,偏向锁会升级为轻量级锁,等待锁的线程通过 CAS 自旋避免进入阻塞状态。

③、重量级锁:如果自旋失败,锁会升级为重量级锁,等待锁的线程会进入阻塞状态,等待监视器 Monitor 进行调度。

详细解释:

①、从无锁到偏向锁:

当一个线程首次访问同步代码时,如果此对象处于无锁状态且偏向锁未被禁用,JVM 会将该对象头的锁标记改为偏向锁状态,并记录当前线程 ID。此时,对象头中的 Mark Word 中存储了持有偏向锁的线程 ID。

如果另一个线程尝试获取这个已被偏向的锁,JVM 会检查当前持有偏向锁的线程是否活跃。如果持有偏向锁的线程不活跃,可以将锁偏向给新的线程;否则撤销偏向锁,升级为轻量级锁。

②、偏向锁的轻量级锁:

进行偏向锁撤销时,会遍历堆栈的所有锁记录,暂停拥有偏向锁的线程,并检查锁对象。如果这个过程中发现有其他线程试图获取这个锁,JVM 会撤销偏向锁,并将锁升级为轻量级锁。

当有两个或以上线程竞争同一个偏向锁时,偏向锁模式不再有效,此时偏向锁会被撤销,对象的锁状态会升级为轻量级锁。

③、轻量级锁到重量级锁:

轻量级锁通过自旋来等待锁释放。如果自旋超过预定次数(自旋次数是可调的,并且是自适应的,失败次数多自旋次数就少),表明锁竞争激烈。

当自旋多次失败,或者有线程在等待队列中等待相同的轻量级锁时,轻量级锁会升级为重量级锁。在这种情况下,JVM 会在操作系统层面创建一个互斥锁——Mutex,所有进一步尝试获取该锁的线程将会被阻塞,直到锁被释放。

四、如何保证有序性和可见性

保证可见性

通过两步操作:

  • 加锁时,线程必须从主内存读取最新数据。

  • 释放锁时,线程必须将修改的数据刷回主内存,这样其他线程获取锁后,就能看到最新的数据。

保证有序性

synchronized 通过 JVM 指令 monitorenter 和 monitorexit,来确保加锁代码块内的指令不会被重排。

实现可重入

可重入意味着同一个线程可以多次获得同一个锁,而不会被阻塞。

synchronized 之所以支持可重入,是因为 Java 的对象头包含了一个 Mark Word,用于存储对象的状态,包括锁信息。底层是通过 Monitor 对象的 owner 和 count 字段实现的,owner 记录持有锁的线程,count 记录线程获取锁的次数。

当一个线程获取对象锁时,JVM 会将该线程的 ID 写入 Mark Word,并将锁计数器设为 1。

如果一个线程尝试再次获取已经持有的锁,JVM 会检查 Mark Word 中的线程 ID。如果 ID 匹配,表示的是同一个线程,锁计数器递增。

当线程退出同步块时,锁计数器递减。如果计数器值为零,JVM 将锁标记为未持有状态,并清除线程 ID 信息。

五、区别联系

1、synchronized 和 volatile 有什么区别?

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

  • volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。

  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

2、synchronized 和 ReentrantLock 的区别?

主要两点:

  • synchronized 由 JVM 内部的 Monitor 机制实现,ReentrantLock基于 AQS 实现。

  • synchronized 可以自动加锁和解锁,ReentrantLock 需要手动 lock() 和 unlock()。

继续细节:

①、ReentrantLock 可以实现多路选择通知,绑定多个 Condition,而 synchronized 只能通过 wait 和 notify 唤醒,属于单路通知;

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

②、synchronized 可以在方法和代码块上加锁,ReentrantLock 只能在代码块上加锁,但可以指定是公平锁还是非公平锁。

// synchronized 修饰方法
public synchronized void method() {
    // 业务代码
}

// synchronized 修饰代码块
synchronized (this) {
    // 业务代码
}

// ReentrantLock 加锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 业务代码
} finally {
    lock.unlock();
}

③、ReentrantLock 提供了一种能够中断等待锁的线程机制,通过 lock.lockInterruptibly()来实现。

ReentrantLock lock = new ReentrantLock();
try {
    lock.lockInterruptibly();
} catch (InterruptedException e) {
    // 处理中断异常
}

并发量大的情况下,使用 synchronized 还是 ReentrantLock?

我更倾向于 ReentrantLock,因为:

  • ReentrantLock 提供了超时和公平锁等特性,可以应对更复杂的并发场景。

  • ReentrantLock 允许更细粒度的锁控制,能有效减少锁竞争。

  • ReentrantLock 支持条件变量 Condition,可以实现比 synchronized 更友好的线程间通信机制。

参考链接:

浅析synchronized锁升级的原理与实现 - 小新成长之路 - 客园

0

评论区