偏向锁、轻量级锁、自旋锁、重量级锁

参考:https://www.jianshu.com/p/36eedeb3f912https://www.cnblogs.com/mingyao123/p/7424911.html

锁的重量级别是:偏向锁-> 轻量级锁、自旋锁-> 重量级锁 

偏向锁

偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS

“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁

轻量级锁

轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗

顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁

自旋锁

首先,内核态与用户态的切换上不容易优化。但通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)。

  • 当前线程竞争锁失败时,打算阻塞自己
  • 不直接阻塞自己,而是自旋(空等待,比如一个空的有限for循环)一会
  • 在自旋的同时重新竞争锁
  • 如果自旋结束前获得了锁,那么锁获取成功;否则,自旋结束后阻塞自己 

“锁的持有时间比较短”这一条件可以放宽。实际上,只要锁竞争的时间比较短(比如线程1快释放锁的时候,线程2才会来竞争锁),就能够提高自旋获得锁的概率。这通常发生在锁持有时间长,但竞争不激烈的场景中。

缺点

  • 单核处理器上,不存在实际的并行,当前线程不阻塞自己的话,旧owner就不能执行,锁永远不会释放,此时不管自旋多久都是浪费;进而,如果线程多而处理器少,自旋也会造成不少无谓的浪费。
  • 自旋锁要占用CPU,如果是计算密集型任务,这一优化通常得不偿失,减少锁的使用是更好的选择。
  • 如果锁竞争的时间比较长,那么自旋通常不能获得锁,白白浪费了自旋占用的CPU时间。这通常发生在锁持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁。

使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。

自适应自旋锁

自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:

  • 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。
  • 相反的,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。

重量级锁

内置锁是JVM提供的最便捷的线程同步工具,在代码块或方法声明上添加synchronized关键字即可使用内置锁。使用内置锁能够简化并发模型。

内置锁在Java中被抽象为监视器锁(monitor)。在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

 1 public class StringBufferRemoveSync {
 2  
 3     public void add(String str1, String str2) {
 4         //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
 5         //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
 6         StringBuffer sb = new StringBuffer();
 7         sb.append(str1).append(str2);
 8     }
 9  
10     public static void main(String[] args) {
11         StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
12         for (int i = 0; i < 10000000; i++) {
13             rmsync.add("abc", "123");
14         }
15     }
16  
17 }

synchronize的可重入性:

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。

 

偏向锁的获取、膨胀和释放

1. 偏向锁是一种乐观锁

2. 新new出的对象具有偏向性

3. 第一个线程来访问,新对象就偏向该线程

4. 对象持有偏向锁。

5. CAS操作,对象头中ThreadId指向线程ID。

6. 第一个线程重入时候,只需要对比ID,不需要CAS操作。

7. 第二个线程,检测到对象持有偏向锁,同时检测第一个线程的状态是否存活。注意:偏向锁,不会主动释放。等到某个安全点,如GC安全位置,就会暂停原线程,检测原线程状态。

8. 第一个线程还存活,并持有对象,这个时候偏向锁升级为轻量级锁及偏向锁膨胀的过程。

9. 如果第一个线程已经不存在,第二个线程会把对象置位无锁状态,重新偏向第二个线程。 注意: 两个线程的情况也可能继续为偏向锁。

参考:http://m.sohu.com/a/331159262_120210224

 

拓展:第7点中,当前线程如何检测之前拥有锁的线程是否存活?

 

 

上图原文地址:https://juejin.im/post/5bfe6eafe51d4524f35d04d1

 

posted @ 2019-09-08 16:53  绿色森林  阅读(591)  评论(0编辑  收藏  举报