博主个人博客网站:文客
这个系列会长期更新!
如果你想每天和我打卡面试题、交流技术,可以关注一下我的个人博客网站:文客,我会每天在这里更新技术文章和面试题,也会及时收到大家的评论与留言,欢迎各位大佬来交流!
自旋锁
在很多情况下,共享数据的锁定状态只会维持很短的一段时间,这个时候去挂起和恢复线程就造成了没必要的开销,为了避免这种情况,可以让另一个没有获取到锁的线程循环等待一会,持有锁的线程很快就会释放锁,通过循环自旋等待就减少了不必要的挂起和恢复的开销。这就是自旋锁的由来。
如果锁占用的时间非常短,那么自旋锁的性能会非常的好,反之自旋锁会消耗很多CPU资源。所以一般自旋锁都会有一个限度,自旋到达一定的次数就会被挂起,在JDK中,默认的自旋次数是10次,我们也可以通过参数来修改。
自适应自旋锁
在自旋锁中有一个问题,如果自旋锁刚刚达到了自旋次数而被挂起时,持有锁的线程释放了锁,这就是很蛋疼的一件事了。
所以在JDK1.6中引入了自适应自旋锁。这就意味着自旋的次数不再固定,而是由上一次在同一个锁上的自旋次数和锁的拥有者状态来决定的。如果在上一次自旋获取锁的过程中,自旋锁成功获取到了锁,那么JVM会认为通过自旋获取到该锁的概率很大,于是会增加自旋的次数。如果通过自旋很少获取到锁,那么以后获取到该锁时,就会忽略掉自旋的过程,避免CPU资源浪费。有了自适应自旋,JVM对程序的锁的状态预测会越来越准确,JVM也会越来越聪明。
锁消除
锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持。意思就是:JVM会判断在一段程序中的同步明显不会逃逸出去从而被其它线程访问到,那JVM就把他们当作栈上的数据对待,认为这些数据是线程独有的,不需要加同步。此时就会进行锁消除。
当然在实际开发中,我们很清楚的知道那些的方时线程独有的,不需要加同步锁,但是在Java API中有很多方法都是加了同步的,那么此时JVM会判断这段代码是否需要加锁。如果数据并不会逃逸,则会进行锁消除。比如如下操作:在操作String类型数据时,由于String是一个不可变类,对字符串的连接操作总是通过生成的新的String对象来进行的。因此Javac编译器会对String连接做自动优化。在JDK 1.5之前会使用StringBuffer对象的连续append()操作,在JDK 1.5及以后的版本中,会转化为StringBuidler对象的连续append()操作。
public static String test03(String s1, String s2, String s3) {
String s = s1 + s2 + s3;
return s;
}
上述代码使用javap 编译结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3MZh06wJ-1649683518603)(D:\typora-img\image-20220331173320081.png)]
众所周知,StringBuilder不是安全同步的,但是在上述代码中,JVM判断该段代码并不会逃逸,则将该代码带默认为线程独有的资源,并不需要同步,所以执行了锁消除操作。(还有Vector中的各种操作也可实现锁消除。在没有逃逸出数据安全防卫内)
锁粗化
原则上,我们都知道在加同步锁时,尽可能的将同步块的作用范围限制到尽量小的范围(只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小。在存在锁同步竞争中,也可以使得等待锁的线程尽早的拿到锁)。
大部分上述情况是完美正确的,但是如果存在连串的一系列操作都对同一个对象反复加锁和解锁,即使没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能操作,此时JVM会将加锁同步的范围扩展(粗化)到整个一系列操作的外部,使整个一连串的操作只需要加锁一次就可以了。
public static String test04(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
比如这个程序,StringBuffer是线程安全的,每个对于每个append操作来说都需要加锁释放锁以及同步,会消耗很多不必要的资源,这时JVM就会扩大锁的范围,使一连串的操作只加一次锁就可以了。
轻量级锁
在JDK 1.6之后引入的轻量级锁,需要注意的是轻量级锁并不是替代重量级锁的,而是对在大多数情况下同步块并不会有竞争出现提出的一种优化。它可以减少重量级锁对线程的阻塞带来的线程开销。从而提高并发性能。
偏向锁
在大多实际环境下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取,那么在同一个线程反复获取所释放锁中,其中并还没有锁的竞争,那么这样看上去,多次的获取锁和释放锁带来了很多不必要的性能开销和上下文切换。
同一个线程反复获取所释放锁中,其中并还没有锁的竞争,那么这样看上去,多次的获取锁和释放锁带来了很多不必要的性能开销和上下文切换。
为了解决这一问题,HotSpot的作者在Java SE 1.6 中对Synchronized进行了优化,引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁。只需要简单的测试一下对象头的Mark Word
里是否存储着指向当前线程的偏向锁。如果成功,表示线程已经获取到了锁。