美文网首页Java基础
并发编程(三)synchronized实现原理

并发编程(三)synchronized实现原理

作者: Timmy_zzh | 来源:发表于2021-01-10 17:21 被阅读0次
    Java线程切换的实质
    • Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙。
      • 这就要从用户态转换到和心态,且状态转换需要花费很多处理器时间

    如下代码:

        private Object lock = new Object();
        private int value = 0;
    
        public void setValue() {
            synchronized (lock) {
                value++;
            }
        }
    
    • 上诉代码中value++ 因为被关键字synchronized修饰,所以会在各个线程间同步执行。
      • 但是value++消耗的时间很有可能比线程状态转换消耗的时间还短。所以说synchronized是java语言中一个重量级的操作

    1.synchronized实现原理

    对象头和Monitor

    对象头

    • Java对象在内存中的布局分为3部分:对象头,实例数据,对齐填充。
    • java代码中,使用new关键字创建一个对象时,jvm会在堆中创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据

    instanceOopDesc的基类为oopDesc类,结构如下:

    class oopDesc {
        friend class VMStructs;
        private:
            volatile markOop  _mark;
            union _metadata {
                wideKlassOop  _klass;
                narrowOop     _compressed_klass;
        } _metadata;
    
    • 其中 __mark 和 metadata 一起组成对象头。
      • _metadata主要保存了类元数据。
      • _mark 是 markOop类型数据,一般称为标记字段(Mark Word),其中主要存储了对象的hashCode,分代年龄,锁标志位,是否偏向锁等

    32位Java虚拟机的Mark Word的默认存储结构如下:

    1.32位jvm的Mark Word默认存储结构.png
    • 默认情况下,没有线程进行加锁操作,所以对象中的Mark Word处于无锁状态。
      • 考虑到jvm的空间效率,Mark Word被设计成一个非固定的数据结构,以便存储更多的有效数据。
      • 他会根据对象本身的状态复用自己的存储空间,如32位jvm下,除了Mark Word的默认存储结构外,还有如下可能存在的结构:
    2.Mark Word其他可能结构.png
    • 从图中可以看出,根据“锁标志位”以及“是否为偏向锁”,Java中的锁可以分为以下几种状态:
    是否偏向锁 锁标志位 锁状态
    0 01 无锁
    1 01 偏向锁
    0 00 轻量级锁
    0 10 重量级锁
    0 11 GC标记
    • 在Java6之前,并没有轻量级锁和偏向锁,只有重量级锁,也就是说synchronized的对象锁,锁标志位为10。
      • 当锁是重量级锁时,对象头中Mark Word会用30bit来指向一个“互斥量”,这个互斥量就是Monitor

    Monitor

    • Monitor可以理解为一个同步工具,也可以描述为一种同步机制。它是一个保存在对象头_mark中的一个对象
    • 在markOop类中有一个方法monitor() 会创建一个ObjectMonitor对象,OjbectMonitor就是Java虚拟机中的Monitor的具体实现。
      • 因此Java中每个对象都会有一个对应的ObjectMonitor对象,这也是Java中所有的Object都可以作为锁对象的原因
    ObjectMonitor实现锁同步机制

    ObjectMonitor结构:

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

    其中几个比较关键的属性:

        _owner:     //指向持有ObjectMonitor对象的线程
        _WaitSet:   //存放处于wait状态的线程队列
        _EntryList://存放处于等待锁block状态的线程队列
        _recursions://锁的重入次数
        _count;     //记录该线程获取锁的次数
    
    • 解析

      • 当多个线程同时访问一段同步代码时,线程首先会进入_EntryList队列中,当某个线程通过竞争获取到对象的monitor后,monitor会把 _owner变量设置为当前线程,同时monotor中的计数器 _count加1,即获得对象锁
      • 若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null, _count自减1,同时该线程进入 _WaitSet集合中等待被唤醒。
      • 若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)
    • 实例演示

    比如有3个线程分别执行以下同步代码块:

        private Object lock = new Object();
    
        public void synchMethod() {
            synchronized (lock) {
                // do something
            }
        }
    

    1.锁对象是lock对象,jvm中每个对象实例都有一个对应的oopDesc实例,oopDesc实例中的标记字段(mark word)中的_mark 属性总有一个对应的ObjectMonitor对象,其内部结构如下:

    3.1.ObjectMonitor内部对象结构.png

    2.现在使用3个线程来执行上面的同步代码块。默认情况下,3个线程都会处于阻塞状态,会存放在ObjectMonitor中的EntrySet队列中,如下所示:

    3.2.默认下线程处于阻塞状态,存放在EntrySet队列中.png

    3.假设线程2首先通过竞争获取到了锁对象,则ObjectMonitor中的Owner指针指向线程2,并将count自加1,结果如下:

    3.3.线程2竞争到锁对象,ObjectMonitor中的Owner指针指向线程2.png

    4.上图中Owner指向线程2,表示线程2已经成功获取到锁(Monitor)对象,其他线程只能处于阻塞(blocking)状态。

    • wait操作:
      • 如果线程2在执行过程中调用wait操作,则线程2会释放锁(Monitor)对象,以便其他线程进入获取锁(Monitor)对象,Owner变量恢复为null,count做减1操作。
      • 同时线程2会被添加到WaitSet集合中,进入等待(waiting)状态并等待被唤醒。结果如下:
    3.4.wait操作实现.png

    5.因为线程2释放了锁,线程1和线程3可以再次通过竞争去获取锁(Monitor)对象,如果线程1通过竞争获取到锁,则重新将Owner指向线程1,如下:

    3.5.阻塞状态下线程竞争后重新获取锁.png

    6.如果在线程1执行过程中调用了notify操作将线程2唤醒。

    • 则当前处于WaitSet中的线程2会被重新添加到EntrySet集合中,并尝试重新获取竞争锁(Monitor)对象。
    • 但是notify操作并不会使线程1释放锁,结果如下:
    3.6.notify操作实现.png

    7.当线程1中的代码执行完毕以后,同样会自动释放锁,以便其他线程再次获取锁对象

    重量级锁
    • ObjectMonitor的同步机制使jvm对操作系统级别Mutex Lock(互斥锁)的管理过程,期间都会转入操作系统内核。
    • 也就是说synochronized实现锁,在“重量级锁”状态下,当多个线程之间切换上下文时,还是一个比较重量级的操作。

    相关文章

      网友评论

        本文标题:并发编程(三)synchronized实现原理

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