关闭

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

标签: java锁优化性能优化
353人阅读 评论(0) 收藏 举报
分类:

概述

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

总结

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

参考资料

  1. OpenJDK9 Hotspot : synchronized 浅析
  2. Java SE1.6 中的 Synchronized
0
0
查看评论

Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

Java并发编程:Synchronized底层优化(偏向锁、轻量级锁) Java并发编程系列: Java 并发编程:核心理论  Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁、偏向锁) Java...
  • wangtaomtk
  • wangtaomtk
  • 2016-08-21 11:47
  • 1579

Java并发——Synchronized优化(轻量级锁、偏向锁)

Synchronized的实现依赖于与某个对象向关联的monitor(监视器)实现,而monitor是基于底层操作系统的Mutex Lock实现的,而基于Mutex Lock实现的同步必须经历从用户态到核心态的转换,这个开销特别大,成本非常高。所以频繁的通过Synchronized实现同步会严重影响...
  • asiaLIYAZHOU
  • asiaLIYAZHOU
  • 2017-07-25 20:42
  • 261

[JAVA修炼之路十]-JVM synchronized原理或优化

synchronized语法:1、synchronized语句;2、synchronized方法 1、monitorenter和monitorexit字节码;依赖于底层的操作系统的Mutex Lock来实现的 2、会被翻译成普通的方法调用和返回指令如:invokevirtual、aretur...
  • yubinglin2008
  • yubinglin2008
  • 2016-05-08 16:38
  • 1550

JAVA Synchronized 关键字锁实例和锁CLASS对象的区别

转自:http://liuinsect.iteye.com/blog/1662839
  • suyu_yuan
  • suyu_yuan
  • 2017-01-13 15:13
  • 990

Synchronized(对象锁)和Static Synchronized(类锁)的区别

Synchronized和Static Synchronized区别 通过分析这两个用法的分析,我们可以理解java中锁的概念。一个是实例锁(锁在某一个实例对象上,如果该类是单例,那么该锁也具有全局锁的概念),一个是全局锁(该锁针对的是类,无论实例多少个对象,那么线程都共享该锁)。实例锁对应的就...
  • cs408
  • cs408
  • 2015-10-06 13:11
  • 9815

透彻理解 Java synchronized 对象锁和类锁的区别

synchronized 加到 static 方法前面是给class 加锁,即类锁;而synchronized 加到非静态方法前面是给对象上锁。这两者的区别我用代码来演示下: 对象锁和类锁是不同的锁,所以多个线程同时执行这2个不同锁的方法时,是异步的。 在Task2 中定义三个方法 doLongTi...
  • zhujiangtaotaise
  • zhujiangtaotaise
  • 2017-02-17 14:34
  • 3344

多线程并发 synchronized对象锁的控制与优化

本文针对用户取款多线程并发情境,进行相关多线程控制与优化的描述.
  • sharpyuce
  • sharpyuce
  • 2013-08-26 12:32
  • 6254

多线程之synchronized锁字符串对象的一个易错点

西城旧梦梦旧人 2017-05-08 21:17 前段时间讲了synchronized锁方法,锁this对象,锁非this对象。具体可以看往期的文章,今天我们来看下锁非this对象(锁String对象)的一个注意点。在Java中是有常量池缓存的功能的,就是说如果我先声明了一个String str...
  • u011277123
  • u011277123
  • 2017-05-12 13:39
  • 1226

Java对象锁和类锁全面解析(多线程synchronized关键字)

最近工作有用到一些多线程的东西,之前吧,有用到synchronized同步块,不过是别人怎么用就跟着用,并没有搞清楚锁的概念。最近也是遇到一些问题,不搞清楚锁的概念,很容易碰壁,甚至有些时候自己连用没用对都不知道。今天把一些疑惑都解开了,写篇文章分享给大家,文章还算比较全面。当然可能有小宝鸽理解得不...
  • u013142781
  • u013142781
  • 2016-06-17 15:51
  • 14479

Java常用面试题15 synchronized方法的妙用 锁池和等待池的区别

synchronized妙用 锁池和等待池的区别
  • HZ_LIZX
  • HZ_LIZX
  • 2017-02-14 11:33
  • 742
    个人资料
    • 访问:604293次
    • 积分:7751
    • 等级:
    • 排名:第3263名
    • 原创:192篇
    • 转载:6篇
    • 译文:0篇
    • 评论:99条
    博客专栏
    文章分类
    最新评论