偏向锁,轻量级锁,重量级锁
Posted stevenczp
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了偏向锁,轻量级锁,重量级锁相关的知识,希望对你有一定的参考价值。
先引入几个概念
1. 对象头
Java中,每个对象都含有一个对象头,用于保存一些额外信息,以32位JDK为例,对象头长度为4byte,其结构如下
锁状态 |
25 bit |
4bit |
1bit |
2bit |
||
23bit |
2bit |
是否是偏向锁 |
锁标志位 |
|||
轻量级锁 |
指向栈中锁记录的指针 |
00 |
||||
重量级锁 |
指向互斥量(重量级锁)的指针 |
10 |
||||
GC标记 |
空 |
11 |
||||
偏向锁 |
线程ID |
Epoch |
对象分代年龄 |
1 |
01 |
|
无锁 |
对象的hashCode |
对象分代年龄 |
0 |
01 |
参见hotspot\\src\\share\\vm\\oops\\markOop.hpp
2. lock record
参见hotspot\\src\\share\\vm\\runtime\\basicLock.hpp
其关键代码如下
class BasicLock VALUE_OBJ_CLASS_SPEC { friend class VMStructs; private: volatile markOop _displaced_header;//对象头 ......................... } class BasicObjectLock VALUE_OBJ_CLASS_SPEC { friend class VMStructs; private: BasicLock _lock; // the lock, must be double word aligned oop _obj; // object holds the lock; .......................... }
也就是BasicObjectLock这个class了,其中记录了锁对象的mark word,与锁对象的指针
重量级锁
这是最重的锁了,直接使用操作系统自带的互斥量来实现
当线程A拥有针对某个对象的重量级锁时,对象头的mark word中保存了指向互斥量的指针,锁状态也被设置为10
如果此时线程B试图获取这个对象的锁,在检测到mark word的状态后,线程B会找到对应的互斥量,将自己注册到这个互斥量的等待队列中,然后挂起自身
线程A解锁时,会唤醒互斥量中等待队列里的线程B,使其可以占用这个对象的锁
轻量级锁
利用系统互斥量是一个很重的操作,根据经验规律我们可以知道:大部分的锁竞争都不激烈,很多情况下锁对象虽然会被多线程使用,但是线程之间不会发生冲突,针对这种情况就有了轻量级锁的优化
所谓的轻量级锁,就是当一个线程试图获取某个Object上的锁时,不是直接调用很重的mutex,而是
1. 先在这个线程的栈帧中创建一个lock record,然后将对象头的mark word复制到这个lock word里
2. cas的修改对象头的mark word,在mark word里写入指向这个线程的指针,并将锁标记位改写成00。写入成功,表示这个对象上加了轻量级的锁,跳转至3。如果写入不成功,表明这个对象被其他的线程以轻量级锁锁住,跳转至5
3. 执行操作,跳转至4
4. 检查对象头的mark word,如果锁状态还是00,那么表明占用期间没有发生争用,可以放心解锁,也就是把栈帧中保存的mark word用cas替换到对象头中。如果锁状态不为00,说明发生了锁争用,轻量级锁已经膨胀成为了重量级锁,现在对象头的mark word里保存的是指向互斥量的指针,走一般的重量级锁的解锁流程即可。
5. 考虑到大部分的锁争用只发生很短时间,先原地自旋若干次,如果还是不能获取锁,就执行锁膨胀操作,将这个对象的轻量级锁升级为重量级锁。具体操作是先申请一个mutex,将对象头的mark word中的指针指向这个mutex,锁状态改写成10,然后将自己放入等待队列,然后挂起自己。从第4步中我们可以知道,当前占有锁的线程在执行完毕之后,会发现这个锁已经膨胀,这个等待中的线程也就会被唤醒。
ps. 如果发生锁重入,线程会在栈帧中再次创建lock record,此时lock record中只记录指向锁对象的指针,mark word位直接置为0。重入锁解锁时,发现mark word位为0的情况,只会删除这个lock record,不会对对象头做任何操作
可以看到,在低竞争的情况下,轻量级锁用轻量级的cas操作替代了重量级的mutex操作,减少了系统开销
偏向锁
根据统计,在实际情况下,大部分的锁对象永远只被一个线程占用,那么在这种情况下,轻量级锁在每次monitorenter和monitorexit的时候(非重入),都会进行一次cas操作,为了进一步减少cas操作,偏向锁(biased locking)诞生了。
所谓的偏向锁,是指某个对象一经加锁,在不发生争用的情况下,mark word里的指针永远是偏向这个线程的,那么在不发生锁争用的情况下,线程每次进入临界区之前,只需要检查一下对象头中的mark word是否是指向自己即可,如果还是指向自己,那么在栈帧中申请一个lock record即可。这样就只用进行一次cas操作了。
但是如果发生竞争该怎么办呢?那就要走revoke biased流程了:
1. 看一眼持有锁的线程是否还活着,如果已经死了,那将对象设置为无锁状态就可以了
2. 如果这个线程还活着,那需要遍历持有这个锁的线程的栈帧中的所有lock record,如果所有lock record都不指向锁对象,那么这个线程实际上不持有这个对象锁。同1,将对象设置为无锁状态即可
3. 如果这个线程正在占用这个锁对象,那么需要修改线程中的锁记录与对象头,将它们都修改为轻量级锁状态,然后正常走轻量级锁的流程即可
总结:
偏,轻,重锁,分别解决三个问题
偏:只有一个线程进入临界区
轻:多个线程交替进入临界区
重:多线程同时进入临界区
ps.
1. 如果对象被调用过native的hashCode方法,那么这个对象的对象头中的hashcode字段就有值了,那么这个对象就无法进入偏向锁状态,就算正处于偏向锁状态,那也要revoke baised了
2. revoke baised是一个相当昂贵的操作,如果应用程序的锁争用极其激烈,偏向锁经常被revoke,那么直接关闭偏向锁可能反而会提高性能
参考资料
JVM内部细节之一:synchronized关键字及实现细节(轻量级锁Lightweight Locking)
聊聊并发(二)Java SE1.6中的Synchronized
以上是关于偏向锁,轻量级锁,重量级锁的主要内容,如果未能解决你的问题,请参考以下文章
javas的四种状态 无锁状态 偏向锁状态 轻量级锁状态 重量级锁状态
Java 并发编程线程锁机制 ( 锁的四种状态 | 无锁状态 | 偏向锁 | 轻量级锁 | 重量级锁 | 锁竞争 | 锁升级 )