并发编程系列之Synchronized实现原理

2022-05-07 18:18:27 浏览数 (1)

并发编程系列之Synchronized实现原理

1、了解synchronized字节码

下面给出一个简单例子,synchronized关键字加在两个方法上,另外一个加在方法里

代码语言:javascript复制
public class SynchroinzedDemo {
    static int a;
    public static synchronized void add1(int b){
        a  = b;
    }

    public synchronized void add2(int b){
        a  = b;
    }

    public void add3(int b){
        synchronized (this){
            a  = b;
        }
    }

    public static void main(String[] args) {

    }
}

先用使用javac编译为class文件,或者在IDE直接运行就行,找到对应class文件,使用如下命令:

代码语言:javascript复制
javap -verbose SynchroinzedDemo.class > log.txt

找到log.txt文件,对比两个加了synchronized关键字的方法,都有ACC_SYNCHRONIZED这个标识

代码语言:javascript复制
public static synchronized void add1(int);
    descriptor: (I)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field a:I
         3: iload_0
         4: iadd
         5: putstatic     #2                  // Field a:I
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0     b   I

  public synchronized void add2(int);
    descriptor: (I)V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=2, args_size=2
         0: getstatic     #2                  // Field a:I
         3: iload_1
         4: iadd
         5: putstatic     #2                  // Field a:I
         8: return
      LineNumberTable:
        line 10: 0
        line 11: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/example/concurrent/sync/SynchroinzedDemo;
            0       9     1     b   I

找到比较关键的monitorentermonitorexit关键字,monitorentermonitorexit关键字是什么?后面再介绍

代码语言:javascript复制
  public void add3(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_0
         1: dup
         2: astore_2
         3: monitorenter
         4: getstatic     #2                  // Field a:I
         7: iload_1
         8: iadd
         9: putstatic     #2                  // Field a:I
        12: aload_2
        13: monitorexit
        14: goto          22
        17: astore_3
        18: aload_2
        19: monitorexit
        20: aload_3
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
      LineNumberTable:
        line 14: 0
        line 15: 4
        line 16: 12
        line 17: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  this   Lcom/example/concurrent/sync/SynchroinzedDemo;
            0      23     1     b   I
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [ class com/example/concurrent/sync/SynchroinzedDemo, int, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

综上:

  1. 方法上加synchronized,是在ACC_SYNCHRONIZED关键字
  2. 方法里加synchronized(obj),对应字节码是monitorentermonitorexit

2、Monitor是什么?

前面的javap编译,我们知道了monitorentermonitorexit,synchronized重量级锁实现依赖于Monitor,所以需要介绍一下说明是Monitor,翻译过来是监视器?我们知道synchronized锁的作用和ReentrantLock的作用是一致的,所以synchronized实现同步的原理是否应该一样?实现上应该也要有互斥量,有等待队列,有重入计数。

前面的学习,我们知道synchronized锁的实现依赖于jvm,要实现锁就要有互斥量,jvm实现锁的方式是什么? jvm也是程序,因为作为java程序和操作系统的中间件,所以可以直接使用操作系统提供的线程同步原语:mutex互斥量和semaphore信号量,当然也可以使用CAS锁

而jvm使用的Monitor又是什么?和jvm以及操作系统底层的线程同步原语又有什么关系?

Monitor,翻译过来可以说是官程,或者说是监视器 在使用操作系统底层的线程同步原语,需要程序员非常小心地控制mutex的down和up操作,否则很容易引起死锁等问题。为了更容易写出正确程序,所以在mutex和semaphore的基础上,提出了更高层次的同步原语monitor,当然monitor并不是操作系统提供的,而是由编译器,比如java的jvm自己去实现的,所以要使用monitor要确定编程语言是否支持,比如c语言就不支持,java语言支持,因为jvm已经实现了,所以说synchronized是jvm层面的锁

jvm如何实现monitor的?可以去github下载openJdk的源码,路径:

代码语言:javascript复制
openjdkhotspotsrcsharevmruntimeobjectMonitor.hpp
penjdkhotspotsrcsharevmruntimeobjectMonitor.cpp

主要类是ObjectMonitor.cpp,看了源码实现:

代码语言:javascript复制
//
// The ObjectMonitor class is used to implement JavaMonitors which have
// transformed from the lightweight structure of the thread stack to a
// heavy weight lock due to contention
// objectMonitor用于实现javaMonitor,javaMonitor是由于线程争用而从线程堆栈的轻量级结构转换为的重量级锁
// It is also used as RawMonitor by the JVMTI
代码语言:javascript复制
// initialize the monitor, exception the semaphore, all other fields
 // are simple integers or pointers
 ObjectMonitor() {
  _header    = NULL;
  _count     = 0;
  _waiters    = 0, // 等待中的线程数
  _recursions  = 0;  //线程重入次数
  _object    = NULL; // 存储该monitor的对象
  _owner     = NULL; //拥有该monitor的线程
  _WaitSet    = NULL; //等待线程组成的双向循环链表,_WaitSet 指向第一个节点
  _WaitSetLock  = 0 ;
  _Responsible  = NULL ;
  _succ     = NULL ;
  _cxq      = NULL ;  //多线程竞争锁进入时的单向链表
  FreeNext    = NULL ;
  _EntryList   = NULL ;  //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
  _SpinFreq   = 0 ;
  _SpinClock   = 0 ;
  OwnerIsThread = 0 ;
  _previous_owner_tid = 0; // 前一个拥有此监视器的线程ID
}

主要方法:

代码语言:javascript复制
 bool    try_enter (TRAPS) ;
 // 重量级锁入口方法
 void    enter(TRAPS);
 // monitor 释放
 void    exit(bool not_suspended, TRAPS);
 // 等待方法
 void    wait(jlong millis, bool interruptable, TRAPS);
 // 唤醒方法
 void    notify(TRAPS);
 void    notifyAll(TRAPS);

synchronized锁队列协作流程比较复杂,所以源码的本博客就不详细描述,读者可以参考博客synchronized实现原理 小米信息部技术团队,里面有对源码做一个比较清晰的分析

3、锁的优化方法

在jdk6中,java虚拟机团队对锁进行了重要的改进,为了优化其性能主要引入了偏向锁、轻量级锁、自旋锁、自适应自旋、锁消除、锁粗化等实现。

3.1、锁的粗化

为了保证多线程的有效并发,会要求每个线程持有锁的尽可能短,大部分情况,上面原则是正确的,但是在实际程序运行过程,可能会有一系列操作对一个对象反复加锁和解锁,或者加锁放在循环体中,这种情况会带来不必要的性能问题,所以jvm会对这种情况进行锁的粗化处理。锁粗化就是将锁的作用范围限制得尽可能小,只在共享数据的作用域中才进行同步加锁。

代码语言:javascript复制
public void doSomething(int size){
        for(int i=0;i<size;i  ){
            synchronized(lock){
            	// do something
            }
        }
    // 锁粗化为,jvm可能会对程序优化,改变synchronized同步锁的作用范围
        synchronized(lock){
            for(int i=0;i<size;i  ){
            }
        }
    }
3.2、锁的消除

通过逃逸分析发现其实没有别的线程产生竞争的可能,别的线程没有临界量的引用,虚拟机会直接去掉这个锁

StringBuffer是线程安全的,因为这个类使用了很多synchronized锁,append方法也是

代码语言:javascript复制
@Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }

例子,所以给一个例子,使用append这个方法,不过只是单独在main里同步调用,因为sBuf变量是本地变量,append方法是同步操作,不会存在竞争(不会逃逸),所以这个程序运行过程jvm可能会进行锁消除,忽略Stringbuffer里的synchronized锁

代码语言:javascript复制
public static String getStr(String str1, String str2) {
        StringBuffer sBuf = new StringBuffer();
        sBuf.append(str1);
        sBuf.append(str2);
        return sBuf.toString();
 }
3.3、偏向锁

jdk6引入了偏向锁来优化无线程争用时性能,偏向也即偏向获得它的线程,无锁化执行。

当一个线程获取到锁后,这把锁就是偏向锁。偏向锁是在对象头中记录一个线程ID,当这个线程再次去获取锁时,会校验是否这个线程,如果是直接获取锁就可以。

偏向锁可以提高带有同步但是无线程争用的程序性能,带有效益权衡性质的优化方法。也就是开启偏向锁并不一定都是有利的,如果程序总是存在多个线程竞争的情况,使用偏向锁反而影响性能,可以使用命令关闭偏向锁

代码语言:javascript复制
-XX:-UseBiasedLocking
3.4、轻量级锁

jdk1.6引进了轻量级锁,轻量级锁是相对于重量级锁使用monitor而言的,前面学习,我们知道monitor是基于操作系统底层的线程同步原语。引进轻量级锁并不是为了替换重量级锁,而是为了在没有多线程竞争的前提下,使用轻量级锁,减少重量级锁使用操作系统底层互斥量带来的性能损耗。

所以轻量级锁适应条件是同一时间线程争用不严重的情况。“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验,如果满足,使用轻量级锁当然可以带来性能提升,如果存在竞争,则可能比重量级锁更慢。思考:轻量级锁使用什么来做互斥量?

答案是cas锁轻量级锁使用对象头中的mark work来做互斥判断

以上是java对象处于5种不同状态时,Mark Work中64个位的表现形式,每一行表示对象处于某种状态时的样子。其中各部分参数的意义:

  • lock:2位的锁状态标记位,该标志位的值表示不同的锁状态,比如01表示正常无锁状态或者偏向锁状态,00表示轻量级锁状态,10表示重量级锁状态。
  • biased_lock:biased_lock为1时表示启动偏向锁,为0时表示对象没有启用偏向锁,biased_locklock配合表示的意义

biased_lock

lock

状态

0

01

无锁

1

01

偏向锁

  • age:表示对象的年龄。在jvm的gc中,对象在survivor区复制一次,年龄加1.jvm可以设置一个阈值,默认是15,对象年龄达到15后会进入老年代,可以通过命令-XX:MaxTenuringThreshold进行设置
  • identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。
  • thread:持有偏向锁的线程ID
  • epoch:偏向锁的时间戳。
  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。

轻量级锁的使用过程:

  1. CAS锁修改mark word的lock标识为00,成功就获得锁,失败就是有竞争,自旋,自旋获取不到,转为重量级锁
  2. 抢到轻量级锁后将mark word保存到执行栈上,释放时CAS还原到对象头上,能还原成功,意味着没线程争用,还原不成功,则表示有线程竞争且阻塞等待了,唤醒等待线程,将mark word 复制给它。
3.5、自旋锁和自适应自旋

自旋是一种获取锁的机制,并不是锁的状态。如果业务场景比较简单,可以比较快完成,同时又有多个处理器的情况,则抢不到锁的线程是可以通过循环自旋的方式去获取锁。jdk1.4.2引入,默认关闭,jdk1.6改为默认开启,开关参数:

代码语言:javascript复制
-XX: UseSpinning

优缺点:如果锁占用时间很短,自旋等待的效果是不错的,反之会耗费处理器资源。同时自旋对处理器数量也有要求,必须要有多个处理器。

自适应自旋:jdk1.6引入了自适应自旋,意味着自旋时间不再固定,而是由前一次在同个锁上的自旋时间及锁的持有者状态来决定的。如果在同一个锁对象上,自旋等待获得锁,并且持有锁的线程正在运行,那么虚拟机就会认为这次自旋是成功的,进而它允许自旋等待更长的时间。如果很少成功获取锁,那在以后去抢锁时可能省略自旋的过程。有了自适应自旋,虚拟机对锁的状况预测会越来越准确。

4、锁的升级过程

锁的升级过程:无锁->偏向锁->轻量级锁->重量级锁

  • 偏向锁:当一个线程获取到锁后,这把锁就是偏向锁,偏向锁是在锁对象的对象头中记录一个线程id,然后该线程再次获取锁时,直接获取就可以
  • 轻量级锁:如果有第二个线程来竞争锁,这时就会升级为轻量级锁,轻量级锁是不会阻塞线程的,其底层是通过自旋实现的。自旋是通过CAS获取一个预期的标识,如果没获取到,就会一直循环获取,获取到标识,也就标识获取到锁
  • 重量级锁:如果轻量级锁一直自旋也获取不到锁,才会升级为重量级锁,重量锁是会阻塞线程的,也称之为重锁

5、参考资料

  • synchronized实现原理 | 小米信息部技术团队
  • 《深入理解Java虚拟机》
  • https://www.cs.princeton.edu/picasso/mats/HotspotOverview.pdf
  • https://wiki.openjdk.java.net/display/HotSpot/Synchronization

0 人点赞