一、几种锁状态
java6为了减少获得锁和释放锁带来得性能消耗,引入了“偏向锁”和“轻量级锁”。在Java 6 之前所有的锁都是重量级锁。所以在Java6(包含Java6)之后,一个对象中有4中锁状态:
他们的级别由低到高依次是:
1.无锁
2.偏向锁
3.轻量级锁
4.重量级锁
1.无锁
无锁就是资源没有任何锁定,任何线程都可以尝试对其修改,如果没有冲突就修改成功并退出,否则就会继续循环尝试。也就是CAS机制
2.偏向锁
偏向锁是指一段同步代码一直被一个线程访问,那么该线程会自动获取锁,降低获取锁的代价。
注:偏向锁只会在遇到其他线程尝试竞争锁是,持有的偏向锁才会释放锁,线程不会主动释放偏向锁
3.轻量级锁
当前锁为偏向锁,被其他线程进行访问时,偏向锁会升级成轻量级锁,其他线程会通过自旋形式获取锁,不会阻塞线程,但是自旋会对CPU产生消耗,如果⼀直获取不到锁的话,那该线程就⼀直处在⾃旋状态,白白浪费CPU资源。所以自旋次数一般会设置默认值10次
4.重量级锁
在轻量级锁状态下,如果有线程自旋超过默认次数,会导致锁升级成重量级锁,并自旋线程进入阻塞状态。或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
二、锁升级
重点介绍下锁升级过程!
1.Java对象头
锁是基于对象的,所以我们先来了解下对象头的结构,以及锁信息存放在什么位置
每个Java对象都有对象头。如果是⾮数组类型,则用2个字宽来存储对象头,如果是数组,则会用3个字宽来存储对象头。在32位处理器中,⼀个字宽是32位;在64位虚拟机中,⼀个字宽是64位。对象头的内容如下表:
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode或锁信息等 |
32/64bit | Class Metadata Address | 存储到对象类型数据的指针 |
32/64bit | Array length | 数组的⻓度(如果是数组) |
依照上表格:锁信息在Mark Word字段信息中存储
Mark Word格式:
锁状态 | 29 bit 或 61 bit | 1 bit 是否是偏向锁? | 2 bit 锁标志位 |
---|---|---|---|
无锁 | 0 | 01 | |
偏向锁 | 线程ID | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 此时这⼀位不⽤于标识偏向锁 | 00 |
重量级锁 | 指向互斥量(重量级锁)指针 | 此时这⼀位不⽤于标识偏向锁 | 10 |
GC标记 | 此时这⼀位不⽤于标识偏向锁 | 11 |
从上述表格中,我们可以看到,当对象为偏向锁时,Mark Word存储的时偏向线程的ID,当状态为轻量级锁时, Mark Word 存储的是指向线程栈中 Lock Record 的指针;当状态为重量级锁时, Mark Word 为指向堆中的monitor对象的指针。
2.锁的原理
①.偏向锁
偏向锁会偏向于第⼀个访问锁的线程,如果在接下来的运行,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,偏向锁在资源⽆竞争情况下消除了同步语句,连CAS操作都不做了,提⾼了程序的运行性能。
⼤⽩话就是对锁置个变量,如果发现为true,代表资源无争,则⽆需再⾛各种加锁/解锁流程。如果为false,代表存在其他线程竞争资源,那么就会走。
实现原理
⼀个线程在第⼀次进⼊同步块时,会在对象头和栈帧中的锁记录⾥存储锁的偏向的线程ID。当下次该线程进⼊这个同步块时,会去检查锁的Mark Word⾥⾯是不是放的自己的线程ID。
如果是,表明该线程已经获得了锁,以后该线程在进⼊和退出同步块时不需要花费CAS操作来加锁和解锁 ;如果不是,就代表有另⼀个线程来竞争这个偏向锁。这个时候会尝试使⽤CAS来替换Mark Word里面的线程ID为新线程的ID,这个时候要分两种情况:
-
成功,表示之前的线程不存在了,Mark Word⾥⾯的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
-
失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的方式进⾏竞争锁。
CAS: Compare and Swap ⽐较并设置。
⽤于在硬件层⾯上提供原子性操作。在Intel处理器中,⽐较并交换通过指令cmpxchg实现。⽐较是否和给定的数值⼀致,如果⼀致则修改,不⼀致则不修改。
线程竞争偏向锁的过程如下:
图中涉及到了lock record指针指向当前堆栈中的最近⼀个lock record,是轻量级锁按照先来先服务的模式进⾏了轻量级锁的加锁。
撤销偏向锁
偏向锁使⽤了⼀种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁。 偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,这个过程看起来容易,实则开销还是很大的,大概的过程如下:
1.在⼀个安全点(在这个时间点上没有字节码正在执行)停⽌拥有锁的线程。
2.遍历线程栈,如果存在锁记录的话,需要修复锁记录和Mark Word,使其变成无锁状态。
3.唤醒被停⽌的线程,将当前锁升级成轻量级锁。
-XX:UseBiasedLocking=false。
下⾯这个经典的图总结了偏向锁的获得和撤销:
②.轻量级锁
多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM采用轻量级锁来避免线程的阻塞与唤醒。
轻量级锁的实现原理
JVM会为每个线程在当前线程的栈帧中创建⽤于存储锁记录的空间,我们称为Displaced Mark Word。如果⼀个线程获得锁的时候发现是轻量级锁,会把锁的Mark Word复制到自己的Displaced Mark Word⾥⾯。
然后线程尝试⽤CAS将锁的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
自旋是需要消耗CPU的,如果⼀直获取不到锁的话,那该线程就⼀直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。
但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
自旋也不是⼀直进行下去的,如果自旋到⼀定程度(和JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁。
轻量级锁的释放:
在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word⾥⾯。如果没有发⽣竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。
⼀张图说明加锁和释放锁的过程:
④.重量级锁
重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对比较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU。
---前⾯说到,每⼀个对象都可以当做⼀个锁,当多个线程同时请求某个对象锁时,对象锁会设置几种状态用来区分请求的线程:
Contention List:所有请求锁的线程将被首先放置到该竞争队列
Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
Wait Set:那些调⽤wait方法被阻塞的线程被放置到Wait Set
OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Owner:获得锁的线程称为Owner
!Owner:释放锁的线程
当⼀个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成⼀个 ObjectWaiter 对象插⼊到Contention List的队列的队首,然后调用 park 函数挂起当前线程。
当线程释放锁时,会从Contention List或EntryList中挑选⼀个线程唤醒,被选中的线程叫做 Heir presumptive 即假定继承⼈,假定继承人被唤醒后会尝试获得锁,但 synchronized 是非公平的,所以假定继承⼈不⼀定能获得锁。这是因为对于重量级锁,线程先自旋尝试获得锁,这样做的目的是为了减少执⾏操作系统同步操作带来的开销。如果自旋不成功再进⼊等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平,还有⼀个不公平的地⽅是自旋线程可能会抢占了Ready线程的锁。
如果线程获得锁后调用 Object.wait ⽅法,则会将线程加⼊到WaitSet中,当被 Object.notify 唤醒后,会将线程从WaitSet移动到Contention List或EntryList中去。需要注意的是,当调⽤⼀个锁对象的 wait 或 notify 方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。
总结锁的升级流程
每⼀个线程在准备获取共享资源时:
第⼀步,检查Mark Word⾥⾯是不是放的⾃⼰的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。
第⼆步,如果Mark Word不是⾃己的ThreadId,锁升级,这时候,⽤CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Mark word的内容置为空。
第三步,两个线程都把锁对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作, 把锁对象的MarK word的内容修改为自己新建的记录空间的地址的方式竞争Mark Word。
第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋 。
第五步,⾃旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果自旋失败 。
第六步,进⼊重量级锁的状态,这个时候,⾃旋的线程进⾏阻塞,等待之前线程执行完成并唤醒自⼰。
各种锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执⾏⾮同步⽅法⽐仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适⽤于只有⼀个线程访问同步块场景。 |
轻量级锁 | 竞争的线程不会阻塞,提⾼了程序的响应速度。 | 如果始终得不到锁竞争的线程使⽤⾃旋会消耗CPU。 | 追求响应时间。同步块执⾏速度⾮常快。 |
重量级锁 | 线程竞争不使⽤⾃旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。同步块执⾏速度较⻓。 |