美文网首页首页投稿(暂停使用,暂停投稿)
Java 同步原语 synchronized 剖析和锁优化

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

作者: albon | 来源:发表于2017-09-10 10:37 被阅读131次

    [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

    相关文章

      网友评论

        本文标题:Java 同步原语 synchronized 剖析和锁优化

        本文链接:https://www.haomeiwen.com/subject/wmuzjxtx.html