金歌 发表于 2024-5-21 16:37:38

京东二面:Sychronized的锁升级过程是怎样的

引言

Java作为主流的面向对象编程语言,提供了丰富的并发工具来帮助开发者解决多线程环境下的数据同等性问题。此中,内置的关键字"Synchronized"饰演了至关重要的角色,它能够确保在同一时刻只有一个线程访问特定代码块或方法,从而有效地防止数据竞争和保持内存可见性。
在传统的Synchronized实现中,由于其接纳的是重量级锁机制,每次获取和释放锁都涉及操作系统层面的线程调度,这无疑增长了线程上下文切换的开销,尤其在高并发且锁竞争较小的场景下,可能会导致不须要的性能损失。为此,从Java 6开始,JVM引入了锁升级机制,这是一种动态调整锁状态的技能,旨在根据不同场景灵活运用不同级别的锁,从而在保证并发安全性的同时,最大程度地提拔步伐的运行服从。
关于Synchronized的实现原理,请参考:美团一面:说说synchronized的实现原理?问麻了。。。。
本文将深入探究"Synchronized"的锁升级过程,具体介绍从无锁状态到方向锁、轻量级锁,直至重量级锁的不同阶段及其背后的原理。
Synchronized锁的基础概念

在Java中,synchronized关键字是实现线程同步的关键机制之一,它用于确保多个线程在访问共享资源时的精确性和同等性。synchronized锁的基本思想是,当一个线程进入某个synchronized代码块或方法时,它必须起首获取到该对象或类的锁,然后才能实行相应的操作。如果其他线程试图进入相同的synchronized区域,它们将被壅闭,直到锁被释放。
对象头与Mark Word简介

Java对象在内存中不仅包含类实例的字段,还包含一些元数据,这些元数据存储在对象头中。对象头是Java对象的重要组成部分,它包含了关于对象的重要信息,如哈希码、GC年龄以及锁状态等。此中,Mark Word是对象头中的一个关键字段,它记录了关于对象锁状态的信息。通过修改Mark Word的内容,JVM能够实现对对象锁的获取和释放。
Synchronized锁定的基本原理与运作机制概述

synchronized锁定的基本原理是通过对对象或类的监视器(Monitor)举行加锁息争锁操作来实现线程同步。当一个线程尝试进入synchronized代码块或方法时,它会起首尝试获取对象或类的锁。如果锁已经被其他线程持有,则该线程将被壅闭,直到锁被释放。synchronized锁的运作机制包括方向锁、轻量级锁和重量级锁三种状态。方向锁适用于单线程访问的情况,轻量级锁适用于多线程竞争不猛烈的情况,而重量级锁则用于处理高竞争场景。通过这三种状态的转换,synchronized锁能够根据不同的并发场景动态调整锁计谋,以实现高效的线程同步。
关于synchronized的实现方式,原理介绍,请参考:美团一面:说说synchronized的实现原理?问麻了。。。。
锁升级的概念

锁升级是指Java虚拟机(JVM)在并发环境下对synchronized关键字所使用的锁机制举行动态调整的过程,从最初的无锁状态逐渐过渡到方向锁、轻量级锁,直至最终的重量级锁。这一过程旨在根据现实的并发状况选择最适合的锁类型,以实现对共享资源的最佳保护和最有效的并发控制。
锁升级的主要目的是为了提拔并发性能,减少不须要的线程上下文切换和内存消耗。线程上下文切换是一个相对昂贵的操作,因为它涉及到保存当前线程的状态、恢复另一个线程的状态等一系列操作。通过优化锁计谋,JVM可以减少这种切换的频率,从而提高系统的整体性能。
另外,锁升级也有助于减少内存消耗。相较于重量级锁需要创建额外的Monitor对象并在操作系统层面举行线程调度,方向锁和轻量级锁在一定程度上低落了内存消耗,特别是对于大量短生命周期的锁哀求场景。
Synchronized锁的四种状态详解

当我们使用synchronized时,Java虚拟机(JVM)会为每个被同步的对象维护一个锁(或称为监视器锁)。这个锁有四种状态:从级别由低到高依次是:无锁、方向锁,轻量级锁,重量级锁,用于控制多线程对共享资源的访问。
https://coderacademy.oss-cn-zhangjiakou.aliyuncs.com/blogcontent/20240413150038.png
无锁

无锁状态是对象初始化后的默认锁状态,表示对象当前未被任何线程锁定。在这种状态下,对象头的锁标志位通常为空或特定的无锁标识,表明对象不受任何同步控制,任何线程都能够无停滞地访问该对象。
无锁的标志位为01,即如果是否方向锁标识为0时是无锁状态,为1时是方向锁。在这个状态下,没有线程拥有锁,并且存储了对象的hashcode、对象的分代年龄以及是否为方向锁的标志(0表示不是方向锁)。
当一个线程首次尝试获取锁时,JVM会检查这个锁是否处于无锁状态。如果是,JVM会尝试将锁方向给这个线程,也就是将锁标志为方向这个线程,并且将这个线程的ID记录在锁的标志中。这样,当这个线程再次尝试获取锁时,就可以避免一些昂贵的操作,因为JVM可以直接检查锁是否仍旧方向这个线程。
方向锁

当一个线程首次乐成获取一个锁时,锁就进入了方向锁状态。在方向锁状态下,只有持有方向锁的线程才能再次获取这个锁,而不会引起竞争。如果其他线程尝试获取这个锁,方向锁就会升级为轻量级锁。
方向锁的标志位为01,便是否方向锁表标识位为1。与无锁状态的标志位相同,但存储的内容有所不同。方向锁状态下,会存储方向的线程ID、方向时间戳、对象分代年龄以及是否方向锁的标志(1)。
方向锁是一种针对线程独占锁优化的机制,它适用于单一线程长时间、连续地访问同一段同步代码的情况。当某个线程首次获得同步代码块的锁后,Java虚拟机会在对象头的Mark Word中记录该线程的ID,形成方向锁。在此之后,该线程再次进入同步代码块时,无需实行CAS操作等复杂的同步动作,仅需确认Mark Word中的方向线程ID是否为自己,便可灵敏获得锁,从而极大地减少了获取锁的开销,提拔了并发性能。
在方向锁生效期间,除非有其他线程尝试获取该锁,否则持有方向锁的线程不会主动释放锁。当出现锁竞争时,原有的方向锁持有者会经历打消过程。此过程发生在全局安全点,即在全部线程均停止实行字节码的时刻,JVM会暂停当前持有方向锁的线程,检查锁对象的状态。如果发现持有方向锁的线程不再活动大概锁确实处于被争夺状态,则会打消方向锁,即将对象头恢复为无锁状态(标志位为01)或直接升级为轻量级锁(标志位调整为对应轻量级锁的状态)。
方向锁主要是为相识决在一个线程连续多次获取同一锁的情况,低落不须要的同步操作开销。当首次获取锁的线程再次进入同步代码块时,会检查对象头中存储的线程ID是否与当前线程同等。如果同等,则直接获得锁;如果不同等,则需要打消方向锁,重新举行锁竞争,可能升级为轻量级锁。
优点:
对于没有或很少发生锁竞争的场景,方向锁可以显著减少锁的获取和释放所带来的性能消耗。
缺点:

[*]额外存储空间:方向锁会在对象头中存储一个方向线程ID等相关信息,这部分额外的空间开销固然较小,但在大规模并发场景下,累积起来也可能成为可观的本钱。
[*]锁升级开销:当一个方向锁的对象被其他线程访问时,需要举行打消(revoke)操作,将方向锁升级为轻量级锁,乃至在更高竞争情况下升级为重量级锁。这个升级过程涉及到CAS操作以及可能的线程挂起和唤醒,会带来一定的性能开销。
[*]适用场景有限:方向锁最适合于绝大部分时间只有一个线程访问对象的场景,这样的情况下,方向锁的开销可以降到最低,有利于提高步伐性能。但如果并发程度较高,大概线程切换频仍,方向锁就可能不如轻量级锁或重量级锁高效。
轻量级锁

当一个线程尝试获取一个已经被其他线程持有的方向锁时,方向锁会升级为轻量级锁。轻量级锁是一种用于处理线程之间轻量级竞争的机制。当一个线程尝试获取轻量级锁时,它会先自旋一段时间,尝试等待锁被释放。如果在这段时间内锁被释放了,那么这个线程就可以乐成获取锁。如果自旋结束后锁仍旧被持有,那么这个线程就会尝试将锁升级为重量级锁。
轻量级锁的标识位为:00。当锁从方向锁升级为轻量级锁时,标志位会变为00。在轻量级锁状态下,多个线程可能会尝试获取锁,通过自旋来等待锁被释放。
轻量级锁利用CAS操作尝试将对象头的Mark Word替换为指向线程栈中锁记录的指针,如果CAS操作乐成,则表示线程乐成获取锁。获取锁失败的线程会进入自旋状态,不断循环尝试获取锁,直到获取乐成或升级为重量级锁。在自旋期间,线程不会立即进入壅闭状态,而是不断循环检查锁是否可用。这种机制可以减少线程上下文切换的开销,但如果自旋次数过多大概竞争加剧,自旋就会失去意义,JVM会选择升级为重量级锁。
优点:

[*]低开销:轻量级锁通过CAS操作尝试获取锁,避免了重量级锁中涉及的线程挂起和恢复等高昂开销。
[*]快速响应:在无锁竞争大概锁竞争不猛烈的情况下,轻量级锁使得线程可以灵敏获取锁并实行同步代码块。
缺点:

[*]自旋消耗:当锁竞争猛烈时,线程可能会长时间自旋等待锁,这会消耗CPU资源,导致性能下降。
[*]升级开销:如果自旋等待超过一定阈值大概锁竞争加剧,轻量级锁会升级为重量级锁,这个升级过程自己也有一定的开销。
重量级锁

当轻量级锁的自旋尝试达到一定阈值,大概检测到多个线程竞争猛烈时,JVM会将轻量级锁升级为重量级锁。升级过程中,会取消当前线程的自旋操作,并在对象头中设置重量级锁标志。
重量级锁的标识位为:10。当锁从轻量级锁升级为重量级锁时,标志位会变为10。在重量级锁状态下,线程在获取锁时会壅闭,直到持有锁的线程释放锁。
在重量级锁状态下,线程在获取锁失败时会被操作系统挂起,放入到该对象关联的监视器(Monitor)的等待队列中,由操作系统举行线程调度,当锁被释放时,操作系统会选择符合的线程将其唤醒并授予锁。
尽管重量级锁的开销较大,涉及到线程上下文切换和内核态用户态的切换等,但它在高竞争场景下能提供稳固的互斥性和公平性,确保数据的同等性和线程的安全实行。因此,纵然性能消耗较高,也是在特定情况下须要的权衡步调。
优点:

[*]强同等性:重量级锁提供了最强的线程安全性,确保在多线程环境下数据的完整性和同等性。
[*]简单易用:synchronized关键字的使用简洁明了,不易出错。
缺点:

[*]性能开销大:获取和释放重量级锁时需要操作系统介入,可能涉及线程的挂起和唤醒,造成上下文切换,这对于频仍锁竞争的场景来说性能代价较高。
[*]延迟较高:线程获取不到锁时会被壅闭,导致等待时间增长,进而影响系统响应速度。
以上四种锁状态优缺点对比总结如下:
类型优点缺点使用场景方向锁快速:无须线程上下文切换,适合单一线程多次重复获取同一线程锁的场景
低开销:只需要检查对象头标志不适合多线程竞争的场景
竞争时需要打消方向锁,有一定开销大多数时间只有一线程访问同步代码块,很少出现锁竞争的情况轻量级锁较快:通过CAS操作和自旋避免了线程的壅闭与唤醒,减少了线程上下文切换
适用于锁竞争不猛烈的场景自旋可能导致CPU空耗,在高竞争下,大量的线程自旋会增长系统负担。
无法保证绝对的公平性短时间的同步代码块,且锁竞争不猛烈,期望快速重入和释放重量级锁稳固可靠:严格保证互斥性和公平性
能够有效应对高度竞争的锁场景开销大:涉及到线程上下文切换,性能较低
壅闭线程可能导致响应时间变长高并发、高竞争的场景,需要保证数据同等性,且线程等待锁的时间较长或不可预知关于Java中锁的分类,以及各种所得介绍,请参考:阿里二面:Java中_锁的分类_有哪些?你能说全吗?
关于Java中怎样定位以及避免死锁,请参考:阿里二面:怎样定位&避免_死锁_?连着两个面试问到了!
锁升级的具体步调与流程

1.无锁到方向锁的升级流程:

[*]当线程首次尝试获取对象锁时,JVM起首检查对象是否处于无锁状态。
[*]若处于无锁状态,JVM则立即将其标志为方向锁,并记录下当前线程的ID。
[*]这一过程通过CAS操作实现,确保线程安全地更新对象头的Mark Word为方向锁状态,并保存方向线程的ID。
[*]一旦设置乐成,线程便可无阻碍地进入同步代码块,后续再次获取该锁时仅需验证是否仍方向当前线程,无需额外同步操作
而对于方向锁的释放机制:

[*]当持有方向锁的线程正常退出同步代码块时,JVM仅简单地更新对象头的访问计数等相关信息。
[*]由于方向锁的设计初衷是优化同一线程对锁的反复获取,因此它并不会立即释放方向关系,而是假设下一次仍由同一线程获取锁。
2. 方向锁到轻量级锁的升级流程:

[*]当第二个线程尝试获取已被方向的锁时,它会起首校验对象头是否指向当前线程的ID。
[*]若校验失败,表明锁已方向其他线程,此时需要打消方向锁。
[*]打消后,对象会回到无锁状态或过渡至轻量级锁状态。
[*]接着,新线程会尝试在其栈帧中创建锁记录,并使用CAS操作将对象头的Mark Word替换为指向该锁记录的指针。
[*]若CAS操作乐成,线程即获得轻量级锁;若失败,则进入自旋状态,循环尝试获取锁。
对于轻量级锁的释放机制:

[*]持有轻量级锁的线程在退出同步代码块时,会尝试通过CAS操作将对象头恢复为原始状态,即打消锁记录指针的替换。
[*]若CAS操作乐成,则轻量级锁被顺利释放;否则,可能需要进一步的锁升级或处理。
3. 轻量级锁到重量级锁的升级流程:

[*]当轻量级锁的持有线程退出同步代码块并释放锁时,它会尝试将对象头恢复到无锁或方向锁状态。
[*]若存在多个线程竞争锁资源,轻量级锁的释放可能导致自旋线程长时间无法获取锁。
[*]JVM会综合考量自旋次数、竞争猛烈程度以及系统负载等因素,决策是否将轻量级锁升级为重量级锁。
[*]一旦升级为重量级锁,原持有线程必须完成锁的释放。新来的线程将被壅闭,并被加入对象的监视器(Monitor)等待队列,由操作系统负责线程的调度管理。
对于释放重量级锁:

[*]持有重量级锁的线程在退出同步代码块时,会通过调用Monitor的释放操作来唤醒等待队列中的下一个线程。
[*]被唤醒的线程将获得锁并继续实行同步代码,确保资源的顺序访问和线程安全
https://coderacademy.oss-cn-zhangjiakou.aliyuncs.com/blogcontent/20240413154453.png
锁降级与锁消除

锁降级

锁降级通常出现在使用读写锁(如Java中的ReentrantReadWriteLock)的场景中。在多线程环境下,一个线程起首获取到了写锁,那么在它持有写锁期间,任何其他线程都无法获取读锁或写锁,确保了对该资源的独占访问权以举行修改。这个在持有写锁的同时,线程会尝试获取读锁。由于该线程已经持有写锁,所以它可以乐成获取读锁,而不会造成死锁或其他同步问题。然后线程释会放写锁,但仍持有读锁。此时,其他线程可以获取读锁举行读取操作,但无法获取写锁举行写入操作。
锁降级的意义在于,线程在完成写操作后,如果接下来的任务主要是读取而不是继续写入,那么通过降级能够允许其他读线程同时访问资源,提高了系统的并发性能,同时保证了数据同等性,因为全部读线程看到的都是最近一次写操作完成后的同等性视图。锁降级是针对读写锁的一种高级使用方式,用于提拔多读少写的并发场景性能。
锁消除

锁消除(Lock Elimination)是一种由编译器或虚拟机在运行时举行的优化技能,其目的是去除那些不须要的锁操作。当编译器或JVM的即时编译器(JIT Compiler)在分析代码时发现某个锁保护的变量并没有发生现实的共享数据竞争,也就是说,该变量的生命周期仅限于方法内部,不会逃逸出该方法,那么这个锁就可以安全地被消撤除。
例如,如果一段同步代码块中的变量只在栈上分配并且没有其他线程可以直接访问,那么纵然对该变量举行了同步也不会带来任何好处,反而增长了上下文切换和锁获取释放的开销。在这种情况下,JVM可以通过逃逸分析等本领确定该变量不存在共享状态,进而消除对它的同步操作。
锁消除则是编译器和JVM层面的一种优化技能,用于消除不须要的同步,减少锁带来的性能消耗。
总结

Synchronized锁升级机制是Java虚拟机为优化多线程环境下同步操作性能而设计的一种动态调整计谋。通过方向锁、轻量级锁和重量级锁之间的智能转换,JVM可以根据现实的并发状况在低竞争和高竞争场景下分别采取不同的锁计谋,从而有效减少线程上下文切换、内存占用以及CPU空转等问题,提拔系统的整体并发性能。
方向锁适用于单一线程反复访问同一锁的情况,轻量级锁则在轻度竞争场景下通过CAS和自旋优化锁的获取和释放,而重量级锁固然开销较大,但在高强度竞争下提供了严格的互斥性和线程调度的公平性。
本文已收录于我的个人博客:码农Academy的博客,专注分享Java技能干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、步伐员攻略等

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 京东二面:Sychronized的锁升级过程是怎样的