Java的synchronized关键字和锁升级过程详解(上)

用户态与内核态.

JDK早期,synchronized 叫做重量级锁,因为申请锁资源必须通过kernel,系统调用

; he11o. asm
;write(int fd,const void *buffer, size_ t nbytes)

section data
    msg db "Hello", 0xA
    1en equ $ -msg
section .text
g1obal _start
_start:
    mov edx,1en
    mov ecx, msg
    mov ebx, 1 ; 文件描述符1 std_ _out
    mov eax, 4 ;write函数 系统调用号4
    int 0x80
    mov ebx, 0
    mov eax,1 ;exit函数系统调用号
    int 0x80

CAS

compare and swap

compare and exchange

Java的synchronized关键字和锁升级过程详解(上)

ABA 问题简单理解就是你不知道你的女朋友到底经历过几次恋爱,可以通过加版本标识解决.

cas汇编实现

lock cmpchgl就能保证原子性

Java的synchronized关键字和锁升级过程详解(上)

其实 cas,volatile,synchronize 底层实现都是 lock 指令.

对象的内存布局

Java的synchronized关键字和锁升级过程详解(上)

锁升级

Java的synchronized关键字和锁升级过程详解(上)

  • 匿名偏向是偏向锁启动了,但还没有指定线程.

偏向锁在JDK6是默认启用的,但在应用程序启动大概 4秒后才激活


  • 使用 -XX:BiasedLockingStartupDelay=0 参数关闭延迟,立即启动偏向锁
  • 如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过 XX:-UseBiasedLocking=false 参数关闭偏向锁

-X print help on non- standard options

java -XX:+PrintFlagsFinal -version
  • 可以看到有七百多个参数
  • Java的synchronized关键字和锁升级过程详解(上)
  • java -XX:+PrintFlagsFinal —version (查看jvm 的可以配置的参数)
  • java -XX:+PrintFlagsFinal —version grep BiasedLocking(查看jvm 的可以配置的参数)
  • Java的synchronized关键字和锁升级过程详解(上)
  • UseBiasedLocking 是否启动偏向级锁

我们知道synchronized是重量级锁,效率不怎么样,不过在JDK6中对synchronize的实现进行了各种优化,使得它显得不是那么重了,那么JVM采用了那些优化手段呢

锁优化

如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

锁主要存在四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁状态会随着竞争的激烈而逐渐升级。

锁可以升级,但不可降级!这是为了提高获得锁和释放锁的效率。

自旋锁(轻量级锁)

突然有了线程来竞争了,就不是偏向锁了,开始升级为自旋锁。

竞争过程就是看谁能把自己的 id 信息放进 markword 里的 id 即可,通过 CAS 方式。

和偏向锁一样,无需经过os资源,只需JVM即可。

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。

同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这段很短的时间而去频繁阻塞和唤醒线程非常不值得,所以引入自旋锁。


自旋锁,就是让该线程等待一段时间,而不会被立即挂起,看持有锁的线程是否会很快释放锁。


怎么等待呢?执行一段无意义的循环即可(自旋)。


自旋等待不能替代阻塞,先不说对处理器数量的要求,虽然它可以避免线程切换带来的开销,但是它占用CPU时间。

若持有锁的线程很快就释放了锁,那么自旋效果就很好;反之白白浪费CPU资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎。

所以,自旋等待的时间(自旋的次数)必须有个限度,若自旋超过了定义时间,仍未获取到锁,则应该被挂起。

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");
  // 自旋锁(轻量级锁)
  if (mark->is_neutral()) {
    // Anticipate successful CAS -- the ST of the displaced mark must 预料 CAS 成功
    // be visible <= the ST performed by the CAS.
    lock->set_displaced_header(mark);
    if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
      return;
    }
    // Fall through to inflate() ... 如果失败 =》锁膨胀
  } else if (mark->has_locker() &&
             THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    lock->set_displaced_header(NULL);
    return;
  }

  // The object header will never be displaced to this lock,
  // so it does not matter what the value is, except that it
  // must be non-zero to avoid looking like a re-entrant lock,
  // and must not look locked either.
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD,
                              obj(),
                              inflate_cause_monitor_enter)->enter(THREAD);
}

若 CAS 成功就直接 return 了,若失败会执行下面的锁膨胀方法,也没看到自旋操作。

所以轻量级锁 CAS 失败,不会自旋,而是直接膨胀成重量级锁。

自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开启。

在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。


等待的线程超过 CPU 的一半数量,则升级为重量级锁。


若通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚退出时,就释放了锁(假如你多自旋一两次就可以获取锁),是不是很尴尬?

于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。


上一篇:《策略驱动型数据中心——ACI技术详解》——1.2 基于POD的设计


下一篇:组策略命令行工具使用之组策略对象检查工具GPOTOOL