Java 同步原语 synchronized 剖析和锁优化

[TOC]

概述

Java 在语法层面提供了 synchronized 关键字来实现多线程同步,虽然 Java 有 ReentrantLock 等高级锁,但是 synchronized 用法简单,不易出错,并且 JDK6 对其进行了诸多优化,性能也不差,故而依然值得我们去使用。

本文,我们将对 synchronized 对实现进行剖析,分析其实现原理以及 JDK6 引入了哪些锁优化对手段。

synchronized 实现

我们先看一段代码:

public class LockTest {

    public synchronized void testSync() {
        System.out.println("testSync");
    }

    public void testSync2() {
        synchronized(this) {
            System.out.println("testSync2");
        }
    }
}

在这段代码中,分布使用 synchronized 对方法和语句块进行了同步,接下来我们使用 javac 编译后,再用 javap 命令查看其汇编代码:

javap -verbose LockTest.class

  public synchronized void testSync();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String testSync
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/qunar/fresh2017/LockTest;

  public void testSync2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #5                  // String testSync2
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
      LineNumberTable:
        line 13: 0
        line 14: 4
        line 15: 12
        line 16: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  this   Lcom/qunar/fresh2017/LockTest;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [ class com/qunar/fresh2017/LockTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

这里我们省略了关键方法之外对常量池、构造函数等部分,对于 synchronized 方法,仅仅在其 class 文件的 access_flags 字段中设置了 ACC_SYNCHRONIZED 标志。对于 synchronized 语句块,分布在同步块的入口和出口插入了 monitorenter 和 monitorexit 字节码指令。

注意这里有多个 monitorexit ,除了在正常出口插入了 monitorexit,还在异常处理代码里插入了 monitorexit(请看 Exception table )。

在这篇文章 OpenJDK9 Hotspot : synchronized 浅析里,可以看到 monitorenter 的处理逻辑位于 bytecodeInterpreter.cpp 中,其加锁顺序为偏向锁 -> 轻量级锁 -> 重量级锁。最终重量级锁的代码位于 ObjectMonitor::enter 中,enter 调用了 EnterI 方法,EnterI 方法中,调用了线程拥有的 Parker 实例的 park 方法,这一点和 LockSupport 一致,毕竟都是需要底层操作系统支持的。

// in /vm/runtime/objectMonitor.cpp
void ATTR ObjectMonitor::enter(TRAPS) {
    // omit a lot
    for (;;) {
      jt->set_suspend_equivalent();
      // cleared by handle_special_suspend_equivalent_condition()
      // or java_suspend_self()

      EnterI (THREAD) ;
      // omit a lot
    }
}
void ATTR ObjectMonitor::EnterI (TRAPS) {
    Thread * Self = THREAD ;
    // 省略很多
    for (;;) {
        // omit a lot
       
        // park self
        if (_Responsible == Self || (SyncFlags & 1)) {
            TEVENT (Inflated enter - park TIMED) ;
            Self->_ParkEvent->park ((jlong) RecheckInterval) ;
            // Increase the RecheckInterval, but clamp the value.
            RecheckInterval *= 8 ;
            if (RecheckInterval > 1000) RecheckInterval = 1000 ;
        } else {
            TEVENT (Inflated enter - park UNTIMED) ;
            Self->_ParkEvent->park() ;
        }
    }
    // omit a lot   

锁的粒度

锁的粒度是一个很关键的问题,粒度的大小对于线程的并发性能有很大影响,比如数据库中表锁的并发度要比远低于行锁。下面介绍一下 synchronized 几种常见用法中,锁的粒度:

  1. 对于同步方法,锁是当前实例对象。
  2. 对于静态同步方法,锁是当前对象的Class对象。
  3. 对于同步方法块,锁是 synchronized 括号里配置的对象。

锁优化

JVM

JDK6 中为了提升锁的性能,引入了“偏向锁”和“轻量级锁”的概念,所以在 Java 中锁一共有 4 种状态:无锁、偏向锁、轻量级锁、重量级锁。它会随着竞争情况逐渐升级,锁可以升级但不能降级,也就是说不能有重量级锁变为轻量级锁,也不能由轻量级锁变为偏向锁。下面我们介绍一下这几种锁的特点:

场景 优点 缺点
偏向锁 适用于只有一个线程访问同步块的场景 加锁和解锁不存在额外的消耗,和执行非同步方法比仅存在纳秒级的差距 如果线程间存在竞争,会带来额外的锁撤销的消耗
轻量级锁 适用于同步块执行速度非常快的场景 竞争线程不会阻塞,减少了线程切换消耗的时间 如果线程间竞争激烈,会导致过多的自旋,消耗 CPU
重量级锁 使用于同步款执行较慢,锁占用时间较长的场景 竞争激烈时,消耗 CPU 较少 线程阻塞,会引起线程切换

可以说,没有哪一种锁绝对比另一种锁好,各自都有其适合的场景。下面再简单描述一下,这些锁具体是如何实现的:

  1. 偏向锁:对象头的锁标志位置为 1 表示当前是偏向锁,另有 23bit 用来记录当前线程 id。如果一个线程发现当前是无锁状态,会将锁状态改为偏向锁。如果已经是偏向锁,并且记录的线程 id 和当前线程一致,则认为是获得了锁;否则升级锁到轻量级锁。
  2. 轻量级锁:其本质是自旋锁,也就是轮询去获取锁,实际中功能会高级一点儿,有自适应自旋功能,所谓自适应也就是根据竞争激烈程度适当调整自旋次数,以及决定是否升级为重量级锁。
  3. 重量级锁:其底层实现通常就是 POSIX 中的 mutex 和 condition。

代码

前面讲了 JVM 中优化锁的性能的一些方法,这里再扩展一下,在实际的代码编写过程中,使用锁时,有哪些方法可以改进性能?

  1. 减少锁占用时间:减少锁占用时间,能过有效的减少竞争激烈程度,减少到一定程度,就可以使用轻量级锁,代替重量级锁来提升性能。如何减少呢?
  • 减少不必要的占用锁的代码。
  • 降低锁的粒度。
  1. 锁分离:该技术能过降低锁占用时间,或者减少竞争激烈程度。
  • 将一个大锁分解多个小锁,比如从 HashTable 的对象级别锁到 ConcurrentHashMap 的分段锁的优化。
  • 按照同步操作的性质来拆分,比如读写锁大大提升了读操作的性能。
  1. 锁粗化:看起来与锁分离相反,但是它们适用的场景不同。在一个间隔性地需要执行同步语句的线程中,如果在不连续的同步块间频繁加锁解锁是很耗性能的,因此把加锁范围扩大,把这些不连续的同步语句进行一次性加锁解锁。虽然线程持有锁的时间增加了,但是总体来说是优化了的。
  2. 锁消除:根据代码逃逸技术的分析,如果一段代码中的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必加锁。

总结

在实际中,锁的各种优化技术是可以一起使用的。比如在减少锁占用时间后,就可以使用自旋锁代替重量级锁提升性能。

参考资料

  1. OpenJDK9 Hotspot : synchronized 浅析
  2. Java SE1.6 中的 Synchronized
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容