Synchronized的使用场景

反编译测试

  1. 方法【ACC_SYNCHRONIZED
    1. 静态方法:public static synchronized void doSth () ;
    2. 实例方法:public synchronized void doSth () ;
  2. 代码块【monitorenter,monitorexit,monitorexit
    1. 实例对象:synchronized (this){};
    2. class对象:synchronized (Test.class) { };

      反编译: javac Test.java javap -v Test.class

synchronized第一层总结

反编译synchronized的方法以及代码块,我们已经知道:

  1. synchronized代码块是通过monitorenter、monitorexit实现同步的;
  2. synchronized方法是通过ACC_SYNCHRONIZED实现同步的

它们三作用都是啥呢?我们接着剥第二层…

monitorenter、monitorexit、ACC_SYNCHRONIZED

monitorenter

每个对象都与一个monitor 相关联。当且仅当拥有所有者时(被拥有),monitor才会被锁定。执行到monitorenter指令的线程,会尝试去获得对应的monitor,如下:

  1. 每个对象维护着一个记录着被锁次数的计数器, 对象未被锁定时,该计数器为0。线程进入monitor(执行monitorenter指令)时,会把计数器设置为1.
  2. 当同一个线程再次获得该对象的锁的时候,计数器再次自增.
  3. 当其他线程想获得该monitor的时候,就会阻塞,直到计数器为0才能成功。

image.png

monitorexit

  1. monitor的拥有者线程才能执行 monitorexit指令。
  2. 线程执行monitorexit指令,就会让monitor的计数器减一。
  3. 如果计数器为0,表明该线程不再拥有monitor。其他线程就允许尝试去获得该monitor了。

image.png

ACC_SYNCHRONIZED

方法级别的同步是隐式的,作为方法调用的一部分。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。
当调用一个设置了ACC_SYNCHRONIZED标志的方法,执行线程需要先获得monitor锁,然后开始执行方法,方法执行之后再释放monitor锁,当方法不管是正常return还是抛出异常都会释放对应的monitor锁。
在这期间,如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。
如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
image.png

synchronized第二层的总结

获取monitor: monitor的owner中设置了某个线程的id,且monitor的加锁次数加1,则说明该线程获得了monitor.

  • 同步代码块是通过monitorenter和monitorexit来实现,当线程执行到monitorenter的时候要先获得monitor锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。
  • 同步方法是通过中设置ACC_SYNCHRONIZED标志来实现,当线程执行有ACC_SYNCHRONI标志的方法,需要获得monitor锁。
  • 每个对象维护一个加锁计数器,为0表示可以被其他线程获得锁,不为0时,只有当前锁的线程才能再次获得锁。
  • 同步方法和同步代码块底层都是通过monitor来实现同步的。
  • 每个对象都与一个monitor相关联,线程可以占有或者释放monitor。

好的,剥到这里,我们还有一些不清楚的地方,monitor是什么呢,为什么它可以实现同步呢?对象又是怎样跟monitor关联的呢?客观别急,我们继续剥下一层,请往下看。

monitor监视器

montor到底是什么呢?我们接下来剥开Synchronized的第三层,monitor是什么? 它可以理解为一种同步工具,或者说是同步机制,它通常被描述成一个对象。操作系统的管程是概念原理,ObjectMonitor是它的原理实现。
image.png

同步指的是同步方法/代码块,协作指的是wait/notify机制

操作系统的管程Monitor

  • 管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。
  • 这些共享资源一般是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。
  • 与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。
  • 管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。

    ObjectMonitor

    ObjectMonitor数据结构

    在Java虚拟机(HotSpot)中,Monitor(管程)是由ObjectMonitor实现的,其主要数据结构如下: ```cpp 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 ; }
  1. <a name="OLt46"></a>
  2. ### ObjectMonitor的关键字
  3. ObjectMonitor中几个关键字段的含义如图所示:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1269469/1635928407847-089192b0-ed7f-41fc-8dfd-489facd6e1b8.png#clientId=u288a1155-66cb-4&from=paste&id=ua22a8b45&margin=%5Bobject%20Object%5D&name=image.png&originHeight=517&originWidth=1203&originalType=url&ratio=1&size=187072&status=done&style=none&taskId=u8879f793-8fa0-4573-bf46-51d78e3f2b0)
  4. <a name="JvN6n"></a>
  5. ## synchronized第三层的总结【Monitor 工作机理】
  6. Java Monitor 的工作机理如图所示:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1269469/1635928462756-67a7ea38-e5db-4d87-9740-f3a76dc1e9d3.png#clientId=u288a1155-66cb-4&from=paste&id=u11eb6559&margin=%5Bobject%20Object%5D&name=image.png&originHeight=765&originWidth=1280&originalType=url&ratio=1&size=348528&status=done&style=none&taskId=ua8b0740f-e364-499a-a2a8-9cf1c206794)
  7. - 想要获取monitor的线程,首先会进入_EntryList队列。
  8. - 当某个线程获取到对象的monitor后,进入_Owner区域,设置为当前线程,同时计数器_count加1。
  9. - 如果线程调用了wait()方法,则会进入_WaitSet队列。它会释放monitor锁,即将_owner赋值为null,_count自减1,进入_WaitSet队列阻塞等待。
  10. - 如果其他线程调用 notify() / notifyAll() ,会唤醒_WaitSet中的某个线程,该线程再次尝试获取monitor锁,成功即进入_Owner区域。
  11. - 同步方法执行完毕了,线程退出临界区,会将monitor的owner设为null,并释放监视锁。
  12. 为了形象生动一点,举个例子:
  13. ```java
  14. synchronized(this){ //进入_EntryList队列
  15. doSth();
  16. this.wait(); //进入_WaitSet队列
  17. }

OK,我们又剥开一层,知道了monitor是什么了,那么对象又是怎样跟monitor关联呢?各位帅哥美女们,我们接着往下看,去剥下一层

对象与monitor关联

对象是如何跟monitor关联的呢?直接先看图:
image.png
前面分析可知,monitor特点是互斥进行,你再喵一下上图,重量级锁,指向互斥量的指针。
其实synchronized是重量级锁,也就是说Synchronized的对象锁,Mark Word锁标识位为10,其中指针指向的是Monitor对象的起始地址。
顿时,是不是感觉柳暗花明又一村啦!对象与monitor怎么关联的?答案:Mark Word重量级锁,指针指向monitor地址

Synchronized剥开第四层小总结

对象与monitor怎么关联?

  • 对象里有对象头
  • 对象头里面有Mark Word
  • Mark Word指针指向了monitor

锁优化
事实上,只有在JDK1.6之前,synchronized的实现才会直接调用ObjectMonitor的enter和exit,这种锁被称之为重量级锁。一个重量级锁,为啥还要经常使用它呢? 从JDK6开始,HotSpot虚拟机开发团队对Java中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略。

自旋锁
何为自旋锁?
自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。

为何需要自旋锁?
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒显然对CPU来说苦不吭言。其实很多时候,锁状态只持续很短一段时间,为了这段短暂的光阴,频繁去阻塞和唤醒线程肯定不值得。因此自旋锁应运而生。

自旋锁应用场景
自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。
自旋锁一些思考
在这里,我想谈谈,为什么ConcurrentHashMap放弃分段锁,而使用CAS自旋方式,其实也是这个道理。

锁消除
何为锁消除?
锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。

锁消除一些思考
在这里,我想引申到日常代码开发中,有一些开发者,在没并发情况下,也使用加锁。如没并发可能,直接上来就ConcurrentHashMap。

锁粗化
何为锁租化?
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。

为何需要锁租化?
在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是 为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。

锁租化比喻思考
举个例子,买门票进动物园。老师带一群小朋友去参观,验票员如果知道他们是个集体,就可以把他们看成一个整体(锁租化),一次性验票过,而不需要一个个找他们验票。

总结

我们直接以一张Synchronized洋葱图作为总结吧,如果你愿意一层一层剥开我的心。
image.png