Java多线程--synchronized关键字实现原理(三)

参考文章

这里声明一下,这篇文章完全是从下面这篇文章搬运过来的,所以如果想要原汁原味的解析,请看点击链接,写得超棒!如果侵权则删除。
https://blog.csdn.net/javazejian/article/details/72828483?locationNum=5&fps=1#synchronized%E5%BA%95%E5%B1%82%E8%AF%AD%E4%B9%89%E5%8E%9F%E7%90%86

一、synchronized关键字和对象之间的关系

我之前都是说synchronized关键字用于同步方法和同步代码块时,说一个线程进入了synchronized同步的方法就说线程获取了对象监视器。
1、那么多个线程同时到达临界资源前,是如何获取对象监视器的?
2、每一个对象和对象监视器又是什么关系?
3、以及同步方法和同步代码块之间的字节码之间的不同点又在哪呢?

1.1、对象和对象监视器之间的关系

我们知道一个类的实例对象保存在Java虚拟机的堆内存中,一个实例对象由三个部分组成,分别是对象头、实例变量、填充的数据。
实例变量:存放类的属性信息包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按照4字节对齐。
填充数据:由于虚拟机要求对象气质地质必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
对象头是实现synchronized的锁对象的基础,一般而言synchronized使用的锁对象是存储在Java对象头里。JVM采用两个字来存储对象头(如果这个对象是一个数组,那么会采用3个字来存储对象头,多出来的1个字记录数组长度)。对象头主要结构是由Mark World 和 Class Metadata Address组成。组成结构如下:

虚拟机位数 头对象结构 说明
32/64bit Mark World 存储对象的hashCode、锁信息或分代年龄或GC标志信息
32/64bit Class Metadata 类型指针指向对象的类元数据

这里我们来详细看一下Mark World 默认 情况下32bit结构的划分(对象默认状态是没有和对象监视器相关联的)

锁状态 25bit 4bit 1bit是否偏向锁 2bit锁标志位
无锁状态 对象hashCode 对象分代年龄 0 01

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构

锁状态 25bit 4bit 1bit 2bit
轻量锁 指向栈中锁记录的指针 00
重量锁 指向互斥量的指针 10
GC标志 11
偏向锁 线程ID Epoch 对象分代年龄 1

这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

以上的所有分析简而言之就是:对象持有指向ObjectMonitor的指针

1.2、线程是如何获得对象监视器的

根据上面的ObjectMonitor数据结构我们可以知道,当有多个线程需要访问获取同一个对象的对象监视器时,只有一个一个线程能够获取成功。其中获取ObjectMonitor成功的线程,_owner这个变量就会保存一个指向该线程的指针,而其他线程就会保存在_EntryList当中,同时count会+1。如果线程调用wait()方法,那么这个时候当前线程会是释放对象锁,也就是_owner置为null,线程就会被添加到_WaitSet当中等待被唤醒。这个时候_EntryList中的等待的线程就会竞争获取monitor。
在这里我根据我自己的一点理解,我们说一个线程获取了对象的monitor,其实倒不如说是一个线程被对象的Monitor设置成为了一个独占Monitor的线程。(和ReentranLock一样)

二、synchronized方法和synchronized代码块底层字节码实现

2.1 synchronized方法

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。下面我们看看字节码层面如何实现:

    public synchronized void  addCount(){
        count ++;
    }
  public synchronized void addCount();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field count:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field count:I
        10: return
      LineNumberTable:
        line 19: 0
        line 26: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   LConcurrence/GetSyncAndNonSync;
2.1 synchronized代码块
  public void addCount();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: dup
         6: getfield      #2                  // Field count:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field count:I
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             4    16    19   any
            19    22    19   any
      LineNumberTable:
        line 19: 0
        line 20: 4
        line 21: 14
        line 29: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      25     0  this   LConcurrence/GetSyncAndNonSync;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class Concurrence/GetSyncAndNonSync, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

三、synchronized可重入性

可重入锁指的是当前线程持有对象的ObjectMonitor,然后当前线程在没有释放锁的情况下再一次申请同一个对象的ObjectMonitor。这个线程不需要进入_EntryList,而是直接获取ObjectMonitor,与此同时count需要+1。待到当前线程执行完当前逻辑以后会退出ObjectMonitor,同时count会执行-1操作,一旦count变为零,然后_Owner就置为null不再保存指向该线程的指针。

扫描二维码关注公众号,回复: 2695709 查看本文章
public class SynchronizedReentranTest {
    public static void main(String[] args){
        SynchronizedReentran synchronizedReentran = new SynchronizedReentran();
        new Thread(synchronizedReentran).start();
    }
}

class SynchronizedReentran implements Runnable{
    private int count = 5;
    public synchronized  void method1(){
        System.out.println(Thread.currentThread().getName()+"进入method1");
        if(--count>0){
            method1();
        }
        System.out.println(Thread.currentThread().getName()+"退出了method1");
    }

    @Override
    public void run() {
        method1();
    }
}

输出

Thread-0进入method1
Thread-0进入method1
Thread-0进入method1
Thread-0进入method1
Thread-0进入method1

从上面的例子我们可以看到,如果一个synchronized不是一个可重入锁,那么这段程序将进入死循环。因为线程在第一次进入method方法的时候并没有完全执行完毕就再一次进入了method方法,如果这个时候需要synchronized不可重入,那么再一次进入这个线程就会因为前一个方法没有执行完线程没有释放ObjectMonitor,导致死锁的发生。为synchronized添加了可重入的特性就防止了线程获取自己占有的ObjectMonitor不会发生死锁。

猜你喜欢

转载自blog.csdn.net/makeliwei1/article/details/81303730