HotSpot虚拟机的锁优化

面试中多次被问到synchronized关键字的实现原理,一直认为仅是monitorentermonitorexit两条指令而已,原来底层涉及到多种锁优化策略,包括:自旋锁,轻量锁,偏向锁。

1、自旋锁

互斥同步对性能影响最大的部分是线程的阻塞与恢复,因为这两个操作涉及用户态与内核态的转换。如果共享数据锁定时间很短,而且竞争不是特别激烈,那么阻塞实现并不划算。因此在多核的处理器中,我们可以让请求锁的线程进行忙等,而不是放弃处理器执行时间。反之,如果锁占用时间比较长,那忙等的线程只会白白浪费处理器资源,因此需要有一定的时间限制,超出限制便挂起线程。

使用AtomicReference模拟自旋锁:

public class SpinLock {

  private AtomicReference<Thread> sign =new AtomicReference<>();

  public void lock(){
    Thread current = Thread.currentThread();
    while(!sign .compareAndSet(null, current));
  }

  public void unlock (){
    Thread current = Thread.currentThread();
    sign .compareAndSet(current, null);
  }
}

2、轻量锁 

轻量锁是相对于阻塞实现的重量锁而言的,很多时候虽然用到锁,但并不存在竞争情况,相当于多个线程轮流获取一把锁。轻量锁可使用CAS操作获取,被多线程竞争时会膨胀为重量锁。

每个对象的对象头中都有一个字,称为mark word,低两位代表状态,根据状态的不同,其余各位有不同的含义:

HotSpot虚拟机以一种叫做displaced headers的方法实现轻量锁。当线程获取轻量锁时,会在该线程的栈上创建一个lock recordlock record由一个mark word备份,一个指向对象的指针组成),然后备份当前mark word,最后使用CAS操作把mark word的状态位更新为Thin locked,也就是00,同时mark word中的指针指向lock record

  

发生重入时,线程可以知道对象mark word中的指针正指向自己的栈,便创建一个值为NULLlock record,在释放锁时如果lock record值为NULL就立即返回。

当轻量锁被一个线程获得后,另一个希望获取锁的线程会将轻量锁膨胀为重量锁,该线程在一个循环里不断尝试,直到膨胀成功。膨胀也会发生在锁的waitnotify方法被调用时,无论该锁是否已被一个线程获取。

进行膨胀时,首先用CASmark word中的状态位更新为Inflating,尝试获取处于该状态锁的线程会等待到膨胀完成,已经获取到锁的线程也必须等到膨胀完成才能释放锁。

3、偏向锁

前文说到轻量锁适合的场景是多个线程轮流占用资源,而偏向锁适用的场景是一个线程反复占用资源。什么时候会出现这种情况呢?比如在使用一些类库时,它的接口是线程安全的,但我们不会通过多个线程访问。

在Unlocked状态下,mark word的第三位代表是否允许启用偏序。如果那一位为0,说明锁没有被任何线程获取,且不允许偏序。如果为1,锁可以是如下状态中的一种:

  • 匿名偏向:线程指针为NULL,锁还没有偏向于任何一个线程,这是允许偏向的锁的初始状态。
  • 可重偏向:mark word中的epoch字段无效,获取锁的线程可使锁偏向自己。
  • 已偏向:线程指针不为NULL,epoch字段有效,锁正偏向于线程指针指向的线程。

由于偏向锁需要占用hashcode字段存放线程指针,访问该对象的hashcode将会导致偏向状态被撤销(Object类的hashcode的方法会导致这种情况)。

偏向锁使用的lock record与轻量锁一致,但是mark word备份没有被使用,在偏向被撤销时,会被转化为轻量锁。

 

偏序撤销在系统到达全局安全点时执行,竞争线程通过遍历偏序线程的lock record判断锁是否正在被占用, 将其转化为轻量锁或偏向自己。

 

 

参考:《Evaluating and improving biased locking in the HotSpot virtual machine》

posted @ 2018-04-23 16:40  mmmmar  阅读(482)  评论(0编辑  收藏  举报