Java JVM 锁优化

synchronized 的实现方式是用 Monitor 进行加锁,这是一种互斥锁,为了表示它对性能的影响我们称之为重量级锁。

Java 的线程是映射到操作系统原生线程之上的,要阻塞或唤醒一个线程就需要操作系统的协助,让线程从用户态转换到内核态,而状态转换需要耗费 CPU 很多的时间。

 

锁优化仅在 Java 虚拟机 server 模式下起作用

 

自旋锁

Java 虚拟机的开发工程师们在分析过大量数据后发现:共享数据的锁定状态一般只会持续很短的一段时间,为了这段时间去挂起和恢复线程其实并不值得。

自旋锁在 JDK 1.4 中引入,在 JDK 1.6 中默认开启,可以使用 -XX:+UseSpinning 参数来开启。

自旋等待虽然避免了线程切换的开销,但自旋的线程要占用处理器时间的,所以若锁被占用的时间很短,自旋等待的效果就会非常好,反之锁被占用的时间很长,那么自旋的线程只会白白消耗 CPU 资源。

因此自旋等待的时间必须要有一定的限度,超过限定的次数仍然没有成功获得锁,就应当挂起(阻塞)线程了。自旋次数的默认值是 10 次,可以使用参数 -XX:PreBlockSpin 来更改。

 

自适应自旋

在 JDK 1.6 中引入了自适应自旋锁。

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

如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。

如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。

OpenJDK 源码位置:https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/tip/src/share/vm/runtime/objectMonitor.cpp#l2021

static int Knob_FixedSpin          = 0 ;
static int Knob_PreSpin            = 10 ;      // 20-100 likely better

#define TrySpin TrySpin_VaryDuration

void ATTR ObjectMonitor::EnterI (TRAPS) { // 线程没有获取到锁,开始阻塞当前线程
    Thread * Self = THREAD ;
    assert (Self->is_Java_thread(), "invariant") ;
    assert (((JavaThread *) Self)->thread_state() == _thread_blocked   , "invariant") ;

    if (TryLock (Self) > 0) { // 再尝试获取锁
        assert (_succ != Self              , "invariant") ;
        assert (_owner == Self             , "invariant") ;
        assert (_Responsible != Self       , "invariant") ;
        return ;
    }

    DeferredInitialize () ;

    if (TrySpin (Self) > 0) { // 阻塞之前尝试自旋
        assert (_owner == Self        , "invariant") ;
        assert (_succ != Self         , "invariant") ;
        assert (_Responsible != Self  , "invariant") ;
        return ;
    }

    // 省略部分代码
}

int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) {
    int ctr = Knob_FixedSpin ; // 自旋的固定次数
    if (ctr != 0) {
        while (--ctr >= 0) { // 每次减一
            if (TryLock (Self) > 0) return 1 ;
            SpinPause () ;
        }
        return 0 ;
    }

    for (ctr = Knob_PreSpin + 1; --ctr >= 0 ; ) { // 适应性自旋
      if (TryLock(Self) > 0) {
        int x = _SpinDuration ;
        if (x < Knob_SpinLimit) {
           if (x < Knob_Poverty) x = Knob_Poverty ;
           _SpinDuration = x + Knob_BonusB ; // 修改自旋时间
        }
        return 1 ;
      }
      SpinPause () ; // 进入自旋等待
    }

    // 省略部分代码
}

 

锁消除

在动态编译同步块的时候,JIT 编译器可以借助一种被称为逃逸分析(Escape Analysis)的技术来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。从而取消对这部分代码的同步。

// 实际代码,hollis 的引用不会“逃逸”到 f()方法之外,其他线程无法访问到它,锁也就没有了意义
public void f() {
    Object hollis = new Object();
    synchronized(hollis) {
        System.out.println(hollis);
    }
}
// JIT 编译后代码
public void f() {
    Object hollis = new Object();
    System.out.println(hollis);
}

// 和上面例子相同,sb 的作用域其他线程无法访问,append 方法的同步会被 JIT 编译消除掉
public String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}
View Code

 

锁粗化

当 JIT 编译器发现一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会将加锁同步的范围扩散(粗化)到整个操作序列的外部。

在编写代码的时候,总是推荐将同步块的作用范围(锁粒度)限制得尽量小(只在共享数据的实际作用域中才进行同步),这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程可以尽快的拿到锁。 

锁粒度:不要锁住一些无关的代码。锁粗化:可以一次执行完的不要多次加锁执行

public Object object = new Object();
// 源代码
public void f() {
    for (int i = 0; i < 100000; i++) {
        synchronized (object) {
            System.out.println("xx");
        }
    }
}
// JIT 编译后代码
public void f() {
    synchronized (object) {
        for (int i = 0; i < 100000; i++) {
            System.out.println("xx");
        }
    }
}


public StringBuffer sb = new StringBuffer();
// 和上面例子一样,会把 append 操作的锁提到上一层,让三个 append 操作只加一次锁
public String concatString(String s1, String s2, String s3) {
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}
View Code

https://www.jianshu.com/p/f05423a21e78


https://www.hollischuang.com/archives/2344

https://icyfenix.iteye.com/blog/1018932

posted @ 2019-06-06 11:08  江湖小小白  阅读(1116)  评论(0编辑  收藏  举报