1、前言
前面我们介绍了偏向锁,轻量级锁,自旋锁相关知识。初次之外,锁升级过程还会涉及到重量级锁。重量级锁是并发编程中常用的同步机制之一,它能够确保对共享资源的互斥访问,但由于其较高的开销,需要在合适的场景中使用。今天我们就来深入聊聊关于重量级锁,以及他的原理和性能分析。
2、重量级锁
在 JVM 中,重量级锁的实现主要依赖于操作系统提供的底层互斥锁机制。JVM 使用了操作系统的互斥原语(Mutex)来实现重量级锁的功能。
其实现原理:
- 对象头中的 Mark Word:每个 Java 对象在内存中都有一个对象头,其中的 Mark Word 被用于存储对象的锁状态信息。在重量级锁的情况下,Mark Word 中会记录指向重量级锁(即互斥锁)的指针。
- Monitor(监视器):重量级锁的核心是 Monitor 对象,它用于控制对共享资源的访问。每个 Java 对象都有一个与之关联的 Monitor 对象,用于管理对象的锁状态。
- 线程的阻塞和唤醒:当一个线程尝试获取一个被其他线程持有的重量级锁时,它会进入阻塞状态。在 JVM 中,线程的阻塞和唤醒是通过操作系统提供的底层原语实现的。具体来说,当线程无法获取锁时,JVM 会调用操作系统提供的阻塞原语将线程置于等待状态,并将其从运行队列中移除。当锁被释放时,JVM 会通过操作系统提供的唤醒原语将等待的线程重新加入到运行队列中。
- 操作系统的互斥锁:JVM 通过操作系统提供的互斥锁机制来实现对重量级锁的互斥访问。具体来说,JVM 在内部使用操作系统提供的互斥原语(如互斥量、信号量等)来实现对 Monitor 对象的互斥访问。当一个线程获取锁时,JVM 会请求操作系统分配一个互斥锁,并将其锁定,从而保证同一时间只有一个线程可以访问被锁定的 Monitor 对象。
3、数据结构和控制流程
重量级锁的数据结构和控制流程主要涉及 Monitor 对象和线程的阻塞与唤醒。
3.1、Monitor 对象
Monitor 对象是重量级锁的核心数据结构,用于控制对共享资源的访问。每个 Java 对象都会关联一个 Monitor 对象,用于管理对象的锁状态。
Monitor 对象包含了以下信息:
- Owner:记录当前持有锁的线程。
- Entry Set:记录等待获取锁的线程队列,采用先进先出的顺序。
- Wait Set:记录因调用了 wait() 方法而进入等待状态的线程队列,也是先进先出的顺序。
- 计数器:用于记录当前持有锁的线程重入的次数。
3.2、控制流程
- 线程尝试获取锁:
- 当一个线程尝试获取一个被其他线程持有的重量级锁时,它会进入阻塞状态。
- 线程通过检查 Monitor 对象的状态来判断是否可以获取锁。
- 如果锁处于可用状态,线程会成功获取锁,并将 Monitor 对象的 Owner 字段指向当前线程。
- 如果锁已被其他线程持有,当前线程会被放入 Monitor 对象的 Entry Set 中等待。
- 线程释放锁:
- 当线程释放锁时,它会将 Monitor 对象的 Owner 字段置为 null,表示锁已被释放。
- 如果 Monitor 对象的 Entry Set 非空,则会从队列头部取出一个线程唤醒,使其尝试获取锁。
- 线程进入等待状态:
- 当线程调用 wait() 方法时,它会释放持有的锁,并将线程置于 Monitor 对象的 Wait Set 中等待。
- 线程会等待其他线程调用相同 Monitor 对象的 notify() 或 notifyAll() 方法来唤醒它。
4、性能分析
重量级锁在性能方面存在一些局限性。
- 线程阻塞与唤醒开销:当一个线程尝试获取重量级锁时,如果锁已经被其他线程持有,该线程会进入阻塞状态,并被放入等待队列中。当锁被释放时,需要唤醒等待队列中的线程。这涉及到线程的上下文切换、状态转换以及线程间的通信,会带来较大的开销。
- 线程调度开销:重量级锁依赖于操作系统对线程的调度机制。当一个线程被阻塞时,操作系统需要重新调度其他可执行线程,这涉及到上下文切换和线程调度算法,会引入较高的开销。
- 竞争激烈时的争用:重量级锁在高并发环境下,由于需要线程的阻塞与唤醒操作,会引发较大的竞争,导致锁的争用激烈。这可能会导致大量的线程等待锁的释放,降低系统的并发性能。
可以采用一些优化策略:
- 减少锁的持有时间:尽量缩小锁的范围,减少锁的持有时间,以降低线程等待锁的时间。
- 使用细粒度锁:如果可能,将大锁拆分为多个小锁,以减少锁的争用范围。
- 无锁算法和乐观锁:对于一些适合无锁或乐观锁的场景,可以考虑使用无锁算法或乐观锁来避免使用重量级锁。
- 使用其他同步机制:根据具体场景,可以选择适当的同步机制,如读写锁、信号量、并发集合等,来替代重量级锁。
5、同其他锁的对比
其实不难发现,前面几种锁的介绍,到现在重量级锁,这些都是为了保证数据并发安全,而做的一系列锁优化升级,随着竞争情况逐步升级,相应的对于性能的开销也是越来越大。
- 偏向锁:偏向锁是针对只有一个线程访问同步代码块的情况而设计的,它通过在对象头中的标记字段记录锁的信息,避免了多个线程之间的竞争。因此,偏向锁的性能开销非常低,适用于对同步频率较低的场景。
- 自旋锁:自旋锁是在多个线程之间进行忙等待,不会让线程阻塞,而是让线程不断自旋尝试获取锁,以避免线程切换的开销。自旋锁适用于锁竞争时间短、线程数较少的情况,可以有效减少线程上下文切换的开销。
- 轻量级锁:轻量级锁是一种在锁竞争激烈的情况下进行优化的锁机制。它通过使用CAS操作来避免线程阻塞和唤醒的开销,并使用对象头中的标志字段表示锁的状态。轻量级锁在多个线程之间进行自旋等待,如果自旋等待失败,则升级为重量级锁。轻量级锁适用于锁竞争不激烈的情况,可以减少线程切换的开销。
- 重量级锁:重量级锁是一种使用操作系统提供的互斥量实现的锁机制。它涉及到线程的阻塞和唤醒,需要操作系统的介入。重量级锁适用于锁竞争激烈、线程持有锁的时间较长的情况。
性能方面,偏向锁的性能最好,因为它几乎没有额外的开销。自旋锁和轻量级锁的性能较好,它们通过自旋等待来减少线程切换的开销。而重量级锁的性能较差,因为它涉及到线程阻塞和唤醒,需要操作系统的介入。
在JDK6之前,synchronized使用的便是重量级锁。而到了1.6之后,引入了无锁,偏向锁,轻量级锁以及自适应自旋锁等优化。这就有了之后的“无锁->偏向锁→轻量级锁→>重量级锁”的升级过程。