美文网首页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