偏向锁,轻量级锁,重量级锁

先引入几个概念

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,那么直接关闭偏向锁可能反而会提高性能

 

参考资料

java锁优化

JVM内部细节之一:synchronized关键字及实现细节(轻量级锁Lightweight Locking)

聊聊并发(二)Java SE1.6中的Synchronized

虚拟机中的锁优化简介(适应性自旋/锁粗化/锁削除/轻量级锁/偏向锁)

openjdk对biased locking的官方文档

一篇详细讲解了biased locking的论文

LockSupport.park()实现分析

posted @ 2017-01-12 15:43  qeDVuHG  阅读(902)  评论(0编辑  收藏  举报