文章目录
前言
在金庸先生的笑傲江湖里,风清扬也好,令狐冲也罢,其实是传统武侠世界谢幕的挽歌,是信仰自由与个性的武侠精神熄灭前残留的一点火星,它在昏沉压抑的江湖中看起来是那么显眼,不是因为它真的明亮,而是这个世界实在已经太黑暗了。
一、什么是java锁优化升级?
令狐冲:师父,前段时间我和岳不群师父聊了下synchronized的原理和使用,然后我就信心满满的去找田伯光比划了,田伯光问我知不知道锁优化升级,我说不知道,然后他就说我不配和他比划。。
风清扬:嗯,这个知识点确实比较难又比较重要,岳不群都不一定会,自然需要我来给你解答了。
令狐冲:那师父能不能先给我说说啥是锁优化、锁升级?
风清扬:其实,锁优化升级都是针对synchronized来说的,你在前一章节也学习了synchronized的原理,知道了在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,那你觉得这样子做的效率如何?
令狐冲:效率不高吧,因为使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。
风清扬:说的很对,然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境),如果每次都调用Mutex Lock那么将严重的影响程序的性能。
令狐冲:这确实是个很严峻的问题,如果在没有竞争的时候不加锁,只在有竞争的时候加锁就好了。
风清扬:一点就透,这确实就是锁升级最初的思想。
风清扬:其实在 JDK 1.6之前,synchronized 还是一个重量级锁,是一个效率比较低下的锁,但是在JDK 1.6后,Jvm为了提高锁的获取与释放效率对(synchronized )进行了优化,引入了 偏向锁 和 轻量级锁 ,从此以后锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级。
令狐冲:那就是说使用了synchronized,也不会一上来就变成重锁,会视情况而定,看看自己是否需要升级。那锁还可以降级吗?
风清扬:锁升级是不可逆的过程,也就是说只能进行锁升级(从低级别到高级别),不能锁降级(高级别到低级别),意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
令狐冲:那有了这个技术之后,java的高并发能力不是得到增强了吗?
风清扬:是的,从JDK 5到JDK 6,HotSpot团队花费了大量的精力去实现各种锁优化技术,包括偏向锁( Biased Locking )、轻量级锁( Lightweight Locking )和如适应性自旋(Adaptive Spinning)、锁消除( Lock Elimination)、锁粗化( Lock Coarsening )等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。
令狐冲:那师父,你能讲讲锁是如何升级的吗?
风清扬:往下看!!
二、锁如何升级
2.1 锁的四种状态
风清扬:前面说了,锁一共四种状态,无锁、偏向锁、轻量级所、重量级锁,它会随着竞争情况逐渐升级。
风清扬:再看下锁升级的具体过程吧,一定要牢牢记住。
2.2 synchronized在java对象头中的存储
令狐冲:尴尬,看的晕晕乎乎的,你讲的一点都不透彻。而且我都不知道锁存在哪里的?
风清扬:哈哈,那我就先讲讲锁存储的位置,然后再给你具体讲讲锁的升级过程,保证你能听的懂。
风清扬:其实synchronized 用的锁是存在Java对象头里的,那么你知道什么是对象头吗?
令狐冲:这个我还是知道的,在 JVM 中,Java对象保存在堆中时,由三部分组成:
对象头 | 包括了关于堆对象的布局、类型、GC状态、同步状态和标识hash码的基本信息 |
---|---|
实例数据 | 主要是存放类的数据信息,父类的信息,对象字段属性信息 |
对齐填充 | 为了字节对齐,填充的数据,不是必须的 |
风清扬:小子记得不错嘛,就是这个东西。synchronized 用的锁就是存在Java对象头里的!!
令狐冲:师父,这个我也知道,以Hotspot 虚拟机为例,Hopspot 对象头主要包括两部分数据,Mark Word(标记字段) 和 Klass Pointer(类型指针)。
风清扬:Klass Pointer,即类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
风清扬:那你再仔细讲解下MarkWord这个部分吧。
令狐冲:markword用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
风清扬:书背的不错呀,你有没有发现:这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
令狐冲:这点我就听不懂了,师父能不能画张图呀?
风清扬:Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:
风清扬:在64位JVM中是这么存的:
风清扬:虽然它们在不同位数的JVM中长度不一样,但是基本组成内容是一致的。我先解释下里面的名词,以32位JVM为例:
锁标志位(lock) | 区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。 |
---|---|
biased_lock | 是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。 |
分代年龄(age) | 表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。 |
对象的hashcode(hash) | 运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中。 |
偏向锁的线程ID(JavaThread) | 偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。 |
epoch | 偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。 |
ptr_to_lock_record | 轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。 |
ptr_to_heavyweight_monitor | 重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。 |
令狐冲:师父,这个知识点太重要了,那我再以不同锁状态的角度来说说:
无锁 | 对象头开辟 25bit 的空间用来存储对象的 hashcode ,4bit 用于存放对象分代年龄,1bit 用来存放是否偏向锁的标识位,2bit 用来存放锁标识位为01 |
---|---|
偏向锁 | 在偏向锁中划分更细,还是开辟 25bit 的空间,其中23bit 用来存放线程ID,2bit 用来存放 Epoch,4bit 存放对象分代年龄,1bit 存放是否偏向锁标识, 0表示无锁,1表示偏向锁,锁的标识位还是01 |
轻量级锁 | 在轻量级锁中直接开辟 30bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为00 |
重量级锁 | 在重量级锁中和轻量级锁一样,30bit 的空间用来存放指向重量级锁的指针,2bit 存放锁的标识位,为11 |
GC标记 | 开辟30bit 的内存空间却没有占用,2bit 空间存放锁标志位为11。 |
风清扬:小子,你概述的很好,你已经掌握精髓了。
2.3 Monitor
令狐冲:师父,我想问下monitor到底是啥?
风清扬:Monitor 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。
风清扬:Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联,同时 monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
Owner | 初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL |
---|---|
EntryQ | 关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程 |
RcThis | 表示blocked或waiting在该monitor record上的所有线程的个数 |
Nest | 用来实现重入锁的计数 |
HashCode | 保存从对象头拷贝过来的HashCode值(可能还包含GC age) |
Candidate | 用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值,0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。 |
风清扬:并且同时只能有一个线程可以获得该对象monitor的所有权。在线程进入时通过monitorenter尝试取得对象monitor所有权,退出时通过monitorexit释放对象monitor所有权。
令狐冲:这个我知道,Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。
令狐冲:操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为重量级锁。
2.4 锁的升级过程
令狐冲:师父,说了半天了,你仔细给我捋捋锁的升级过程呗,下次我面试的时候也好扯皮呀。
风清扬:好的,你要仔细听了。
风清扬:jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。
令狐冲:师父那你就讲讲具体咋升级的呗~~~
风清扬:别着急,这就讲。咱们先看看无锁状态。如果不加 synchronized 关键字,表示无锁,很好理解。
2.4.1 偏向锁
风清扬:再看下偏向锁,初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
令狐冲:我好像听懂了,偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。
令狐冲:师父,那这个时候,是没有别的线程在竞争吗?只有一个线程在抢占这个资源?
风清扬:偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。
风清扬:不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS原子操作的性能消耗,不然就得不偿失了。
令狐冲:那有几次CAS操作呢?面试被问过的呀。
风清扬:只有在一个线程在获取锁时**,会在 Mark Word 里存储锁偏向的线程 ID,这个时候才会用一次CAS。**在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。
令狐冲:那偏向锁啥时候会释放呀?
风清扬:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
令狐冲:所以是只要是有悲的线程在竞争,就会立即释放吗?
风清扬:你问的很细呀,当然不是啦。偏向锁的撤销,需要等待全局安全点。字面意思就是,安全了再考虑释放锁。暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,然后撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态
令狐冲:那使用偏向锁有啥好处吗?
风清扬:偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。
2.4.2 轻量级锁(自旋锁)
令狐冲:那师傅在说说轻量级锁呗~~
风清扬:轻量级锁是JDK 6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用monitor的传统锁而言的,因此传统的锁机制就称为“重量级”锁,轻量级锁并不是用来代替重量级锁的,因为不是所有的时候开销都比较小,只是在一定的情况下能够减少消耗。
令狐冲:哪种情况才会升级为轻量级锁呀?
风清扬:一般有两种情况: ① 当关闭偏向锁功能时; ② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。
令狐冲:就是说,一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。那师傅说下具体的升级过程呗~
风清扬:1. 首先,判断当前对象是否处于无锁状态(hashcode、0、01),如果是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,将对象的Mark Word复制到栈帧中的Lock Record中,将Lock Reocrd中的owner指向当前对象 。
风清扬:2.然后 ,JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。
风清扬:3. 如果失败,则判断当前对象的Mark Word是否指向当前线程的栈帧;如果是,则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。
令狐冲:师父,如果其余线程检测到当前对象处于锁的状态,那些没有抢到锁的线程会挂起吗?
风清扬:在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。
令狐冲:那我就晓得了。那还有一个问题,自旋操作不是很消耗CPU的吗,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,只能忙等待?
风清扬:这是一个非常核心的问题,如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
2.4.3 重量级锁
令狐冲:那啥时候会升级为重量级锁呢?
风清扬:刚刚提到了,当别的线程抢占不到对象锁的时候,会一直处于忙循环状态,但是这个循环次数是有限制的,不然忙循环消耗资源是很大的。
令狐冲:那自旋锁的默认自选次数是多少呢?可以修改吗?
风清扬:每个线程会有个计数器记录自旋次数,**默认允许循环10次,**可以更改。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。
令狐冲:那就是说一旦升级为重量级锁之后,除了获取到锁的线程外,其余线程都要进入阻塞等待状态。
风清扬:是的呀,这样就将所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源的。
面试必问
- 知道java中的锁优化吗?具体的讲讲?
- 锁升级是可逆的吗?
- 讲讲什么情况下锁需要升级?竞争激烈吗
- 自旋锁有啥缺点吗?
- 你平时会用synchronized吗?为什么?
- 锁在java对象头中是怎么存储的?
===================================================
字节内推:
字节内推〉字节校招开启。简历砸过来!!!!!!!
200多个岗位,地点:北京 上海 广州 杭州 成都 深圳。。
有问题可以直接在公众号中回复,必回答!!!
字节内推码:B1RHWFK
官网校招简历投递通道:https://jobs.toutiao.com/campus/m/position?referral_code=B1RHWFK
===================================================
微信公众号:猿侠令狐冲