synchronized底层原理—Monitor监视器

深入了解synchronized


synchronized——java多线程锁的关键字

1. 作用

  1. 多线程情况下,同步代码的互斥访问
  2. 有效的解决了共享变量的可见性问题
  3. 解决了指令重排序的问题

2. 底层实现原理

小插曲:用idea插件jclasslib查看指令字节码

写一段代码,然后查看其指令字节码

	public static void soDemo2(){
        System.out.println("进入sDemo2");
        synchronized (ThreadDemo.class){
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("执行 static synchronized(Thread.class) sDemo2");
        }
    }
 0 getstatic #8 <java/lang/System.out>
 3 ldc #15 <进入sDemo2>
 5 invokevirtual #10 <java/io/PrintStream.println>
 8 ldc #13 <Thread/synchronizeddemo/ThreadDemo>
10 dup
11 astore_0
12 monitorenter
13 ldc2_w #3 <2000>
16 invokestatic #5 <java/lang/Thread.sleep>
19 goto 27 (+8)
22 astore_1
23 aload_1
24 invokevirtual #7 <java/lang/InterruptedException.printStackTrace>
27 getstatic #8 <java/lang/System.out>
30 ldc #16 <执行 static synchronized(Thread.class) sDemo2>
32 invokevirtual #10 <java/io/PrintStream.println>
35 aload_0
36 monitorexit
37 goto 45 (+8)
40 astore_2
41 aload_0
42 monitorexit
43 aload_2
44 athrow
45 return

基本上发现,synchronized修饰代码块时,是基于monitorentermonitorexit 指令来实现的,并且进入同步代码是monitorenter,退出同步代码是monitorexit 。但是通过查阅可以知道,如果synchronized 修饰方法名的话,编译后会在方法名上生成一个ACC_SYNCHRONIZED 标识来实现同步。

但是在Java中万物皆是对象,所以这些指令会不会和某些对象有关系呢? 果然可以和一个叫Monitor类联系到一块。

所以看一下Monitor 类是否可以发现新的东西。

1. Monitor类

Monitor类是在javax.management.monitor包下的抽象类。

在这里插入图片描述

官方的介绍总是那么的晦涩难懂,所以要去google一下。

Synchronizationis built around an internal entity known as the intrinsic lock ormonitor lock. (The API specification often refers to this entity simplyas a “monitor.”),Every object has an intrinsic lock associated with it.By convention, a thread that needs exclusive and consistent access toan object’s fields has to acquire the object’s intrinsic lock beforeaccessing them, and then release the intrinsic lock when it’s done withthem.

大致意思就是synchronized同步基于的是Monitor监视锁来实现的。

monitor相当于一个对象的钥匙,只有拿到此对象的monitor,才能访问该对象的同步代码。相反未获得monitor的只能阻塞来等待持有monitor的线程释放monitor。可以这样比喻吧,monitorentermonitorexit 对应的就是拿钥匙和还钥匙。

关于Monitor ,它从两个方面来支撑Java线程间的同步,互斥和协作synchronized 获取对象锁来保证同步代码的互斥执行;通过notifynotifyAllwait进行线程之间的协同工作。所以Object 就是一个监视者。这可能就是Java在多线程方面的设计吧。

2. 管程

monitor另一个名字叫做管程。可惜自己的操作系统学的并不好,只能通过google来进行恶补了。

在这里插入图片描述

所以怎么能够更好的理解Java中的监视锁(管程)就是一个问题了,是否采用Java的面向对象思想能更好的理解一些。

类比

首先,每一个对象都有一个属于自己的monitor,其次如果线程未获取到singal (许可),则线程阻塞。object可以比作医院的诊室,monitor 就是负责喊病人的护士,线程则是就诊的病人。

通过护士(监视器)的调度,诊室(synchronized锁住的对象)内只允许进入一个病人(线程),此病人(线程)在当前时间就拥有此诊室(对象)的使用权,也就是获取了许可。病人就诊完毕,则表明归还了诊室的使用权。然后护士再调度下一个等待的病人进入诊室(被阻塞的线程)。

走廊当中等待的病人们 == Wait Set

3. ObjectMonitor

Java(HotSpot)中的Monitor是基于C++实现的,由ObjectMonitor实现的。

// 初始化monitor,除了semaphore,其他字段都是简单的int或者指针类型
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

ObjectMonitor 的主要参数都在里面,从名字上慢慢分析。

  1. _owner:指向持有ObjectMonitor对象的线程
  2. _WaitSet:存放处于wait状态的队列
  3. _EntryList:存放处于等待锁block状态的队列
  4. _recursions:锁的重入次数
  5. _count:用来记录该线程获取锁的次数

_WaitSet和 _EntryList有什么区别呢?

​ 当多个线程同时访问同步代码时,首先进入的就是_EntryList 。当获得对象的monitor时,_owner 指向当前线程,_count进行加1。
​ 若持有monitor的线程调用wait() ,则释放持有的monitor_owner 变为null_count 减1。
​ 同时该线程进入_WaitSet 等待被唤醒。如果执行完毕,也释放monitor

4. _WaitSet

protected:
  ObjectWaiter * volatile _WaitSet; // LL of threads wait()ing on the monitor

5. _EntryList

protected:
  ObjectWaiter * volatile _EntryList ;     // Threads blocked on entry or reentry.

6. ObjectWaiter

class ObjectWaiter : public StackObj {
 public:
  enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ } ;
  enum Sorted  { PREPEND, APPEND, SORTED } ;
  ObjectWaiter * volatile _next;
  ObjectWaiter * volatile _prev;
  Thread*       _thread;
  jlong         _notifier_tid;
  ParkEvent *   _event;
  volatile int  _notified ;
  volatile TStates TState ;
  Sorted        _Sorted ;           // List placement disposition
  bool          _active ;           // Contention monitoring is enabled
 public:
  ObjectWaiter(Thread* thread);

  void wait_reenter_begin(ObjectMonitor *mon);
  void wait_reenter_end(ObjectMonitor *mon);
};

可以看出来_EntryList_WaitSet 都是ObjectWaiter类型。可以看出是一个双向链表的集合( _next,_prev)。

3. 对象头

知道了Monitor 的一些底层,那么Monitor 是在堆内存中实例对象如何实现的?锁信息是存储在Mark Work当中的。

在这里插入图片描述

(网图)

偏向锁和轻量级锁是Java1.6对synchronized的优化,设计目的就是为了减少性能开销。

​ 偏向锁是解决的问题是:大多数时候不需要锁的线程或者一个锁多次由一个线程获取,为了减少获取锁的代价,设置了偏向锁,因此偏向锁不用被释放,下次该线程继续访问的时候无需再获取偏向所了,更像一个初级通行证。

​ 轻量级锁解决的问题是:少量线程竞争同一个资源并且他们的操作时间比较短,因此不需要将线程阻塞(因为阻塞的代价比较大),没有竞争到锁的线程会轮询固定的次数来获取轻量级锁。

1. 轻量级锁

轻量级锁也是自旋锁,是通过CAS操作。

自旋锁与自适应自旋

Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的getter()和setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。

虚拟机的开发团队注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下“,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK1.6中已经变为默认开。自旋等待不能代替阻塞。**自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会浪费处理器资源。**因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当使用传统的方式去挂起线程了。

JDK1.6中引入自适应的自旋锁,自适应意味着自旋的时间不在固定。而是有虚拟机对程序锁的监控与预测来设置自旋的次数。

自旋是在轻量级锁中使用的

来自于https://www.cnblogs.com/deltadeblog/p/9559035.html

补一张自旋锁的流程图

在这里插入图片描述

_cxq是阻塞在entry上最近可达的线程的列表。

2. 重量级锁

重量级锁也就是同步锁或者互斥锁,它依赖于Monitor,而Monitor则依赖于底层操作系统的实现,所以就出现了用户和内核的切换带来的性能开销。通过对象互斥锁的概念来保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”的标记,这个标记用于保证在任何时刻,只能有一个线程访问该对象。

4. 总结

​ Java从出生开始就是基于多线程来设计的,万物皆对象,每个对象又都是一个Monitor。Monitor是线程私有的数据结构,每一个线程都有一个Monitor Record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
​ 为了优化synchronized互斥锁,在其前面增加了偏向锁和轻量级锁来减少用户态和内核态切换所带来的性能开销。

根据Monitor的设计,我们可以知道synchronized是一个可重入锁,因为ObjectMonitor中_recursions属性。再通过思考轻量级锁和重量级锁,发现synchronized是非公平锁,当一个线程想获取锁时,先试图插队,如果占用锁的线程释放了锁,下一个线程还没来得及拿锁,那么当前线程就可以直接获得锁;如果锁正在被其它线程占用,则排队,排队的时候就不能再试图获得锁了,只能等到前面所有线程都执行完才能获得锁。

参考

https://www.cnblogs.com/mingyao123/p/7424911.html

https://blog.csdn.net/qq_33173608/article/details/88202365

https://www.cnblogs.com/webor2006/p/11442551.html

http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9cbafea410f5/src/share/vm/runtime/objectMonitor.hpp

123/p/7424911.html

https://blog.csdn.net/qq_33173608/article/details/88202365

https://www.cnblogs.com/webor2006/p/11442551.html

http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9cbafea410f5/src/share/vm/runtime/objectMonitor.hpp

https://www.cnblogs.com/webor2006/p/11441679.html

猜你喜欢

转载自blog.csdn.net/qq_40788718/article/details/106450724