Java中的Synchronied的锁原理及升级过程, 探究想上厕所的小华是怎么拉裤兜子的
探究想上厕所的小华是怎么拉裤兜子的)
Java的对象头
什么是对象头
首先我们都知道,Java中的对象都是分配在堆中的(不考虑逃逸分析)
对象除了存储成员数据之外,还有对象头,填充数据等…
而对象头主要包括两部分数据:
在32位的jvm中:
Mark Word(标记字段)、占用1个字宽(32位)【重要】
Klass Pointer(类型指针)、占用1个字宽(32位)
Array length(数组长度,只有数组类型才有)。占用1个字宽(32位)
当对象正常状态下,也就是存活,并且偏向锁被关闭的时候
对象头中MarkWord是这样的:
其中:
- 对象的hashcode不必多说,是通过计算出的对象的哈希码
- 分代年龄是4位,比如1111,最高就是15,所以当对象年龄达到15,就会进入老年代
- 1bit是否偏向锁,如果偏向锁关闭,或者对象处于无锁状态下就是0
- 2bit锁标志位,无锁/偏向锁 状态 都为01
接下来看看:什么是偏向锁?
锁的分类
偏向锁
(JDK1.6之前使用synchronized需要将用户态转换成内核态,很慢)
经过研究发现,大所述情况下,锁不仅不存在竞争,而且总是有同一个线程多次获得,为了让线程获得锁的代价变得更低,引入了偏向锁。
需要知道,偏向锁默认是开启的,也就是当你创建一个对象,对象头是偏向锁的状态。
所谓的偏向锁:
当线程访问同步代码时,会在对象头Mark Word和栈帧的锁记录中存储该线程的ID
以后该线程再进入同步块的时候,只需要比对 对象头中记录的是否是当前线程ID
如果是,直接执行同步块即可,这可以降低线程切换的开销。
偏向锁可以被关闭
使用-XX:+UseBiasedLocking=false
偏向锁的状态
先来看一下对象头的形状:
- 匿名偏向(Anonymously biased)
在此状态下线程ID为NULL(0),意味着还没有线程偏向于这个锁对象。第一个试图获取该锁的线程将会面临这个情况,使用原子CAS指令可将该锁对象绑定于当前线程。这是允许偏向锁的类对象的初始状态。
- 可重偏向(Rebiasable)
在此状态下,偏向锁的epoch字段是无效的(与锁对象对应的klass的mark_prototype的epoch值不匹配)。下一个试图获取锁对象的线程将会面临这个情况,使用原子CAS指令可将该锁对象绑定于当前线程。在批量重偏向的操作中,未被持有的锁对象都被至于这个状态,以便允许被快速重偏向。
- 已偏向(Biased)
这种状态下,thread ptr非空,且epoch为有效值——意味着其他线程正在只有这个锁对象。
具体步骤:
- 获取锁步骤:
1.线程A第一次访问同步块时,先检测对象头Mark Word中的标志位是否为01,
依此判断此时对象锁是否处于无所状态 或者偏向锁状态(匿名偏向锁);
2.然后判断偏向锁标志位是否为1,如果不是,则进入轻量级锁逻辑(使用CAS竞争锁),
如果是,则进入下一步流程;
3.判断是偏向锁时,检查对象头Mark Word中记录的Thread Id是否是当前线程ID,
如果是,则表明当前线程已经获得对象锁,以后该线程进入同步块时,不需要CAS进行加锁,
只会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,用来统计重入的次数
退出同步块释放偏向锁时,则依次删除对应Lock Record,但是不会修改对象头中的Thread Id;
注:
偏向锁撤销是指在获取偏向锁的过程中因不满足条件导致要将锁对象改为非偏向锁状态,
而偏向锁释放是指退出同步块时的过程。
4.如果对象头Mark Word中Thread Id不是当前线程ID,则进行CAS操作,企图将当前线程ID替换进Mark Word。
如果当前对象锁状态处于匿名偏向锁状态(可偏向未锁定),则会替换成功
(将Mark Word中的Thread id由匿名0改成当前线程ID, 在当前线程栈中找到内存地址最高的可用
Lock Record,将线程ID存入),获取到锁,执行同步代码块;
5.如果对象锁已经被其他线程占用,则会替换失败,开始进行偏向锁撤销.
这也是偏向锁的特点,一旦出现线程竞争,就会撤销偏向锁;
- 撤销偏向锁过程
6.偏向锁的撤销需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的),暂停持有偏向锁的线程,检查持有偏向锁的线程状态(遍历当前JVM的所有线程,如果能找到,则说明偏向的线程还存活),如果线程还存活,则检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁;
注:
每次进入同步块(即执行monitorenter)的时候都会以从高往低的顺序在栈中找到第一个可用的Lock Record,
并设置偏向线程ID;每次解锁(即执行monitorexit)时候都会从最低的一个Lock Record移 除。
所以如果能找到对应的Lock Record说明偏向的线程还在执行同步代码块中的代码。
7.如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,
则进行校验是否允许重偏向,如果不允许重偏向,则撤销偏向锁,
将Mark Word设置为无锁状态(未锁定不可偏向状态),然后升级为轻量级锁,进行CAS竞争锁;
8.如果允许重偏向,设置为匿名偏向锁状态,
CAS将偏向锁重新指向线程A(在对象头和线程栈帧的锁记录中存储当前线程ID);
9.唤醒暂停的线程,从安全点继续执行代码
轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”。
需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,所说说轻量级锁是为了在线程近乎交替执行同步块时提高性能。
- 获取锁步骤:
1)判断是否处于无锁状态,若是,则JVM在当前线程的栈帧中创建锁记录(Lock )空间,用于存放锁对象中的Mark Word的拷贝,官方称为Displaced Mark Word;否则执行步骤3)。
2)当前线程尝试利用CAS将锁对象的Mark 新为指向锁记录的指针。如果更新成功意味着获取到锁,将锁标志位置为00,执行同步代码块;如果更新失败,执行步骤3)。
3)判断锁对象的Mark 否指向当前线程的栈帧,若是说明当前线程已经获取了锁,执行同步代码,否则说明其他线程已经获取了该锁对象,执行步骤4)。
4)当前线程尝试使用自旋来获取锁,自旋期间会不断的执行步骤1),直到获取到锁或自旋结束。因为自旋锁会消耗CPU,所限的自旋。如果自旋期间获取到锁(其他线程释放锁),执行同步块;否则锁膨胀为重量级锁,当前线程阻塞,等待持有锁的线程释放醒。
- 释放锁步骤:
1)从当前线程的栈帧中取出Displaced Mark Word存储的锁记录的内容。
2)当前线程尝试使用CAS将锁记录内容更新到锁对象中的Mark 。如果更新成功,则释放锁成功,将锁标志位置为01无锁状态;否则,执行3)。
3)CAS更新失败,说明有其他线程尝试获取锁。需要释放锁并同时唤醒等待的线程。
自旋的优缺点
自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好;反之,自旋的线程就会白白消耗掉处理器的资源,它不会做任何有意义的工作,这样反而会带来性能上的浪费。
结论自旋太多,反而浪费性能
为了避免上文的现象,必须给自旋等待的时间(自旋的次数)设定一个限度,例如让其循环10次,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起(进入阻塞状态)。通过参数-XX:PreBlockSpin可以调整自旋次数,默认的自旋次数为10。
自适应自旋
JDK1.6引入自适应的自旋锁,自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:如果在同一个锁的对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。简单来说,就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
重量级锁
如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。Mark Word的锁标记位更新为10,Mark Word指向互斥量(重量级锁)
Synchronized的重量级锁是通过对象内部的一个叫做监视器锁(monitor)来实现的【即文章第一部分说明的实现】,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间。
升级过程(线程小华是怎么一步一步拉裤兜子的)
无锁==>偏向锁
举个例子:
小明去小华家玩耍,中途小明想要上厕所,而小华家只有一个厕所。厕所上门没有写名字
我们将厕所比喻对象的锁、而小明,小华是抢占锁的两个线程、
门上的名字代表偏向的线程ID、上厕所的过程就是执行同步代码块。
如果开启了偏向锁,此时这个厕所是可偏向状态(匿名偏向锁)
此时对象头形如:
threadID(0) epoch(时间戳) age(分代年龄) 1 01
- 1.小明进入厕所,在自己的手上签下记录,关门后~~门上亮起提示【小明】
这就是类似于偏向锁,
签名,相当于往当前线程的栈中添加一条Lock Record(锁记录)
门亮起提示,相当于 对象头记录了 线程的ID(小明的名字)
此时对象头形如:
threadID(小明) epoch(时间戳) age(分代年龄) 1 01
- 2.小明上完了厕所出来,然而门上依旧写着小明的名字,但是手上的记录消失了
上完厕所: 相当于小明线程执行完了同步代码块
依旧写着小明的名字:说明偏向锁不会自己撤销(偏向的线程ID不会变)
手上的记录消失了:相当于 删除了Lock Record,代表同步块执行完了
对象头形如:
threadID(小明) epoch(时间戳) age(分代年龄) 1 01
这个时候解释什么叫偏向锁:
就是偏袒,偏心的意思
一旦小明还想上厕所,一看门上的名字是自己,直接就进去了
偏向锁==>轻量级锁
继续上一话题
- 3.小华也要上厕所,如果发现门上没有名字,会尝试把门上的名字,换成自己
门上没有名字: 未锁定,未偏向(匿名偏向锁),则会替换成功
(将Mark Word中的Thread id由匿名0改成当前线程ID,在当前线程栈中找到内存地址最高的可用Lock Record, 将线程ID存入
如果小华成功的将名字换成了自己,那么获得了偏向锁,和上文小明的情况一样,这里不多赘述
- 4.但是如果小华发现门上的名字不是自己(假设是小明),也不是空,小华会替换失败
门上的名字不是自己: 锁处于已偏向,锁定状态
替换失败,则开始执行偏向锁的撤销~~
- 5.找到所有人不动的一刹那,使用定身术,将小明定住 ,检查小明死没死
没死的话,看看小明手里的签名记录
这是就是出现了竞争,
1.找到所有人都不动的一刹那:在该状态下所有线程都是暂停的。叫做 全局安全点
2.使用定身术,将小明定住 :就是==>暂停拥有偏向锁的线程
3.检查小明死没死:遍历当前JVM的所有线程,如果能找到,则说明偏向的线程还存活
4.没死:
情况1:看手里的签名(Lock Record),签名存在。说明正在执行同步代码,需要升级为轻量级锁
情况2:签名(Lock Record)不存在,说明执行完了同步代码
- 6 . 小明死了或者签名不存在,就将门上的名字擦除,替换成小华~~~
小明没死,并且签名存在,开始下一次升级【升级为轻量级锁】,并且继续执行步骤7
门上名字擦除,替换成小华: 小华获得了偏向锁,不多赘述,和上文一样
死了/签名不存在:
校验是否允许重偏向,如果不允许重偏向,则撤销偏向锁,将Mark Word设置为无锁状态
如果允许重偏向,设置为匿名偏向锁状态(将门上的名字清除),CAS将偏向锁重新指小华
- 7.唤醒小明让他继续上厕所
唤醒暂停的线程,从安全点继续执行代码。
轻量级锁 ==> 重量级锁
继续上一话题!
- 1.此时小明需要将门上的名字等等(Mark Word)…写到自己右手的第三个手指上
然后进行替换,把门上的名字换成【右手的第三个手指】
写到自己右手的第三个手指上:
就是先把锁对象的对象头MarkWord复制一份到该线程的栈帧中,创建一个用于存储锁记录的空间
(称为DisplacedMarkWord)
门上名字替换为【右手的第三个手指】:
这相当于一个引用
CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的内存地址
此时的对象头形如:
30bit | 2 bit
指向栈中锁记录的指针 | 00
- 2.小明继续上厕所,从安全点开始执行
此时对象头如上文所说,代表已经获取到了轻量级锁
- 3.别忘了,小华还在等待呢,刚才就是因为小华的争夺,导致了锁膨胀!
于是小华,一遍一遍的拽门,拽了好多次(比如10次)之后,发现还是进不去,于是小华不想再等了。
总是进不去:
明有人上厕所时间很长(同步代码执行时间很长)
或者好多人都抢着上厕所(总是有很多线程抢占)
一遍一遍的拽门:
就是在自旋(循环的使用cas,想替换对象头的指针)
注:这里简单说明下自旋,就是使用死循环,重复的去抢锁,例:
for(;;){
CAS替换对象头的指针
}
小华不想再等了:
这是由于自旋会使cpu空转,次数太多了影响性能,所以再一次升级!变为【重量级锁】
- 4.所有想上厕所的人,都进到冷冻仓,冷冻起来,这回没人拽门了 等前一个人上完厕所,将其他人解冻,他们进行了再一次争抢~~
升级成重量级锁之后,会使用到操作系统的一些互斥原语,线程就得阻塞了
全部停止拽门:不再自旋,被系统阻塞了
- 5.前一个人上完厕所了,小华又没争抢到!小华气的大喊:“这不公平啊!明明是我先排队的!!!”
前一个人上完厕所: 执行完同步块,锁被释放了。 不公平:指的是Synchronized是非公平锁 【非公平锁】:线程无论先来后到,后来的线程依旧可以一起去抢锁
终于他拉裤兜子了~