在自己的另一篇文章《JVM学习笔记》中提到了在HotSpot虚拟机中,java对象主要分为对象头、实例信息以及对其填充,而对象头中又进一步包含了Mark Word、类型指针,这里的Mark Word中包含锁状态标志等信息,如下图,本文将进一步说明JAVA中的锁机制。
Hotspot虚拟机对象内存分部.jpg
一、自旋锁和自适应自旋
1.1 自旋锁
一个线程如果执行到synchronized同步方法或者同步代码块时,其他的线程已经持有了该对象的锁,那这个线程就会被操作系统挂起,等待其他线程执行完毕,该线程才能继续执行。然而,线程的挂起和恢复都需要从应用的用户态切换到操作系统的内核态才能完成,这种操作对性能是有很大影响的。另外,很多时候共享数据上的锁很快就能被释放出来,为了这点时间去挂起和恢复线程显得很不值得,这个时候就可以让线程去执行一个忙循环,也就是所谓的自旋,让请求锁的线程在JVM层面“稍等一下”,看持有锁的线程是否能很快将锁释放出来,这种技术,就是所谓的自旋锁。
举一个不太恰当的例子,比如你正在和同事核对一个接口的协议,此时产品喊你到会议室评审需求,你估摸着马上和同事就能对完,所以你跟产品说,“等我一下,马上过来”,而不是将会议挂起,等待下次再组织。这时产品也会先到会议室等待人员到齐(忙循环)。
1.2 自适应自旋
在上面这个例子里面,如果产品最后等了很长时间,参会的人员一直没到齐,那其实对产品自身的时间也是一种浪费,体现在程序上,就是性能的浪费,这也是自旋锁的缺点,所以不能没有限制的自旋,需要更加“智能”的判断,也就是所谓的自适应自旋的概念。
自适应自旋的时间不是固定的,是由前一次自旋时间和持有者的状态来决定的,如果前一次自旋成功获取过锁,则本次自旋等待的时间可以长一些,否则省略自旋,避免资源的浪费。
还是以上面的例子举例,当我跟产品说“等我一下,马上过来”的时候,如果上次会议就因为等我等了很久,产品可能会说,“那算了,等你有空了我们再评审需求”。
二、轻量级锁
为了确保同一时刻,只有一个线程可以操作一段代码,JVM会给每个对象或者类分配一个内置锁,这个锁是通过监视器(Monitors)来实现的,然而这种锁的同步成本非常高,又被成为“重量级锁”。
为了优化重量级锁带来的性能问题,除了上面提到的自旋锁外,如果没有多线程竞争的情况下,申请重量级锁是对性能的浪费,所以JVM提供了一种通过使用CAS(compare and swap)操作,尝试将对象头中的Mark Word指向栈桢中新建立的锁记录(Lock Record)的空间,来表示线程持有了该对象的锁,也就是所谓的轻量级锁。
轻量级锁获取锁的操作步骤
1 JVM在当前线程的栈桢中新建一个锁记录(Lock Record)的空间,用于存储目标对象的Mark Word的拷贝
2 JVM尝试使用CAS操作将该对象的Mark Word指向Lock Record的指针
3 如果第二步的操作成功,则线程持有了该对象的锁,并且对象的Mark Word状态变成(Light-weight locked,00),表示轻量级锁定
4 如果第二步操作失败,JVM会检查该对象的Mark Word是否已经指向了Lock Record,如果是则直接进入同步代码块执行,否则说明对象已经被其他线程抢占了。如果有两个以上的线程争同一个锁,则该对象的Mark Word的锁标记状态更新为“10”(Heavy-weight locked),即膨胀为重量级锁,轻量级锁不再有效,后续等待锁的线程将直接进入阻塞状态。
三、偏向锁
如果自始至终都只有一个线程在使用锁,那此时连轻量级锁都是对资源的浪费,偏向锁的意思就是这个对象的锁会偏向于第一个获取到它的线程,如果后面没有被其他线程获取,则偏向锁永远也不需要同步。
偏向锁的步骤
1.如果开启了偏向锁,那么当锁对象第一次被线程获取的时候,JVM会把对象头的标志位置为“01”,Biased/Biasable,同时CAS操作把获得锁的线程ID记录到Mark Word中,表示该线程持有了该对象的偏向锁
2.如果CAS操作成功,则持有偏向锁的线程每次进入到相关的同步代码块,JVM均不需要进行多余的同步操作
3.当有其他的线程尝试获取这个锁时,偏向锁模式结束,对象头恢复到Unlocked状态或者Light-weight locked(轻量级锁)
四、锁消除和锁粗化
除了上述各种锁外,JVM为了进一步优化,还有锁消除和锁优化两种机制,这里只简单介绍定义
4.1 锁消除
JVM在JIT运行时,对一些代码要求同步而实际该段代码在数据共享上不可能出现竞争的锁而进行消除操作
4.2 锁粗化
对于相同锁定对象的相邻同步代码块,JVM为了提高性能,就会对同步范围进行粗化,把锁放在第一个操作加上,然后在最后一个操作中释放,这样就只加了一次锁但是达到了同样的效果。
网友评论