【Java】从synchronized锁优化来了解自适应自旋锁、锁消除和锁粗化

  • 在jdk1.6之前,【synchronized】是一直都被称为重量级锁;但是在jdk1.6之后,【synchronized】进行了各种优化,本文主要介绍一下以下三种锁:
    • 自适应自旋锁
    • 锁消除
    • 锁粗化

自旋锁和自适应自旋锁:

  • 自旋锁

互斥同步进入阻塞状态的开销都很大,应该尽量避免。

大多数情况下,共享数据的锁定状态持续时间很短,比如:一段逻辑运行完只需要0.0001秒,但是如果使用互斥同步方式,在线程切换上下文时反而会更加耗时。

然而自旋锁的思想是让一个线程在请求一个共享数据的锁时,执行忙循环(自旋),并不会让出CPU的使用,

如果在这段时间可以获得所,就可以避免进入阻塞状态,从而避免上下文的切换。

在jdk1.4中,synchronized自旋锁是默认关闭状态的,并且默认循环次数为10,也就是说

但是自旋锁还有一个这样的弊端:

如果锁被占用很长时间,那么进行忙循环操作占用CPU时间很长,就会造成很大的性能开销,

所以自旋锁只用于共享数据的锁定状态很短的场景。

结合synchronized说一下jdk的自旋锁:

自旋锁在JDK 1.4中引入,默认关闭,但是可以使用【-XX:+UseSpinning】开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数【-XX:PreBlockSpin】来调整; 
如果通过参数-【XX:preBlockSpin】来调整自旋锁的自旋次数,会带来诸多不便。

假如:我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),这特么有点尴尬啊~

于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。
  • 自适应自旋锁
JDK1.6引入了自适应自旋锁,所谓自适应自旋锁,就意味着自旋的次数不再是固定的,具体规则如下:

自旋次数通常由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。如果线程【T1】自旋成功,自旋次数为17次,那么等到下一个线程【T2】自旋时,也会默认认为【T2】自旋17次成功,

如果【T2】自旋了5次就成功了,那么此时这个自旋次数就会缩减到5次。

自适应自旋锁随着程序运行和性能监控信息,从而使得虚拟机可以预判出每个线程大约需要的自旋次数

锁消除:

  • 话不多说,先撸一段代码看看先:
public void add(String str1){
         StringBuffer sb = new StringBuffer("hello");
         sb.append(str1);
}


锁消除,即去除不可能存在共享资源竞争的锁。

众所周知,【StringBuffer】是线程安全的,因为内部的关键方法都是被synchronized修饰过的,

但是上述代码中,sb是局部变量,不存在竞争共享资源的现象,此时JVM会自动需要【StringBuffer】中的锁。

锁粗化:

  • 先看两个实例:
#第一种加锁方式

public
class SynchronizedTest { private int count; public void test() { System.out.println("test"); int a = 0; synchronized (this) { count++; } } }
#第二种加锁方式
public class SynchronizedTest {
    
    private int count;
    
    public void test() {
        synchronized (this) {
            System.out.println("test");
            int a = 0;
            count++;
        }
        
    }

}

通常我们为了降低锁粒度,会选择第一种加锁方式,仅在线程共享资源处加锁,从而使得同步需要的操作数量更少。

  • 而锁粗化思想,就是扩大加锁范围,避免反复加锁,我们再看一下【StringBuffer】的例子:
public String test(String str){
       
       int i = 0;
       StringBuffer sb = new StringBuffer():
       while(i < 100){
           sb.append(str);
           i++;
       }
       return sb.toString():

}

由于【StringBuffer】是在内部方法实现的【synchronized】加锁,我们无法把锁提取到循环体外,如果没有锁粗化,此处要进行100次加锁。

此时JVM会检测到这样一连串的操作都对同一个对象加锁,JVM就会将锁的范围粗化到这一连串的操作的外部(比如while的虚幻体外),使得这一连串的操作只需要加一次锁即可。

原文地址:https://www.cnblogs.com/boluopabo/p/12958284.html