程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

高并发篇_4 synchronized源码分析I

发布于2021-06-12 13:58     阅读(387)     评论(0)     点赞(21)     收藏(0)


高并发篇_4 synchronized源码分析I

田超凡

​​​​​​​原创博文,仿冒必究,部分素材转载自每特教育蚂蚁课堂

1 字节码文件分析Synchronized原理

javap -p -v Thread.class 查看JVM运行代码的指令

  1. Synchronized是重量级锁,也是悲观锁,非公平锁。获取到synchronized锁每次都是全部去竞争,谁抢到就是谁的,没有按顺序抢先来先抢这一说法。在多线程并发的场景下,同一时刻只有一个线程能获取到synchronized锁,其他没有获取到锁的线程会立刻阻塞。Synchronized锁适用于同步代码执行时间比较长,或者CPU资源较为紧张的情况下,因为它不消耗CPU资源,但是每次获取锁和释放锁都需要在用户态和内核态之间进行切换,影响线程执行效率。
  2.  前面分享到Lock锁的升级过程中我们说过,Lock锁升级的最终态就是重量级锁synchronized,当多个线程并发的场景下,当线程重试多次都没有获取到CAS轻量级锁的时候,为了防止出现CPU飙高的问题,避免CPU资源耗尽,我们会将其升级为synchronized重量级锁,升级为synchronized重量级锁的线程会立即阻塞(从用户态切换到内核态,一直阻塞到内核态),并存入到内核态的锁池(EntrySet)中,直到获取到CAS轻量级锁的线程释放之后再唤醒锁池中所有阻塞的线程,重新开始竞争轻量级锁。
  3. Synchronized底层是JVM虚拟机帮助我们封装好的,而JVM虚拟机又是采用C++语言编写的,这也就是说,synchronized底层实际是通过C++指令来运行的。Synchronized锁底层是通过C++中的Monitor和 ObjectMonitor监视器对象来实现,会记录获取到锁的线程、重入次数、锁池和等待池、Monitor关联对象等信息,内部封装了获取锁和释放锁的指令monitorenter和monitorexit
  4. 通过javap 反汇编指令可以得出:

Synchronized底层基于JVM虚拟机中的C++对象Monitor和Object Monitor监视器实现,在Monitor监视器中,C++通过封装好的指令monitorenter和monitorexit来实现获取锁和释放锁

Monitorenter:获取锁-----等效于lock.lock();

Monitorexit:释放锁 ---- 等效于lock.unlock();

同步块的实现使用了monitorenter和monitorexit指令:他们隐式的执行了Lock和UnLock操作,用于提供原子性保证。

  1. 在使用synchronized同步代码快时,底层C++会创建ObjectMonitor监视器对象,并将Monitor监视器中的monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,JVM必须保证每个monitorenter都有一个monitorexit对应,也就是说monitorenter和monitorexit必须是成对出现的。

这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。

  1. 线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行monitorexit,就是释放monitor的所有权。
  2. 在释放锁的代码中,其中monitorexit指令有调用两次,分别代表正常退出和异常退出。下面我们看看这两个指令的官方文档的介绍

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter

 

2 C++ Monitor对象解读

 

3 monitorenter、monitorexit指令解读

monitorenter获取锁:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter 

每一个对象都会和一个监视器C++ monitor关联。

监视器被占用时会被锁住,其他线程无法来获取该monitor。

当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。

其过程如下:

1.若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)

2. 若线程已拥有monitor的所有权,允许它重入monitor,进入monitor的重入数加1

3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

 

monitorexit释放锁:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit

当JVM执行某个线程的某个方法内部的monitorenter获取到锁之后,在同步代码执行完毕后,需要释放锁,它会尝试去释放当前对象占用的monitor的所有权。

其过程如下:

1.能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。

2.执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

 

4 ACC_SYNCHRONIZED 标识同步方法

synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。

Synchronized同步代码块和同步方法的实现原理中,二者的共同点是:

都是基于JVM中的C++监视器ObjectMonitor和Monitor实现,底层都是基于Lock锁升级过程封装的

二者区别在于:

Synchronized同步代码块,调用monitorenter和monitorexit来获取锁和释放锁;

Synchronized 同步方法,通过ACC_SYNCHRONIZED标识来声明为同步方法从而实现获取锁和释放锁。

public static synchronized void count2() {

    System.out.println();

}

 

5 C++监视器ObjectMonitor源码分析

Java底层是使用JVM -> C++ -> HotSpot虚拟机运行的

http://hg.openjdk.java.net/jdk8 下载hotspot虚拟机

synchronized实现原理的核心:ObjectMonitor/Monitor监视器底层都是基于C++实现。

Hotspot 源码位置:

D:\code\hotspot\hotspot\src\share\vm\runtime\objectMonitor.hpp

  1. ObjectMonitor() {
  2.   _header       = NULL;
  3.   _count        = 0;  // 记录个数
  4.   _waiters      = 0,
  5.   _recursions   = 0;   // 递归次数/重入次数
  6.   _object       = NULL;  // 存储Monitor关联对象
  7.   _owner        = NULL; // 记录当前持有锁的线程ID
  8.   _WaitSet      = NULL;  // 等待池:处于wait状态的线程,会被加入到_WaitSet
  9.   _WaitSetLock  = 0 ;
  10.   _Responsible  = NULL ;
  11.   _succ         = NULL ;
  12.   _cxq          = NULL ; // 多线程竞争锁时的单向链表
  13.    FreeNext      = NULL ;
  14.   _EntryList    = NULL ;  // 锁池:处于等待锁block状态的线程,会被加入到该列表
  15.   _SpinFreq     = 0 ;
  16.   _SpinClock    = 0 ;
  17.   OwnerIsThread = 0 ;
  18.   _previous_owner_tid = 0;
  19. }

 

6 锁池和等待池及其切换原理 

  1. 锁池: 假设线程A已经拥有了某个对象的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些获取锁失败阻塞的线程就进入了该对象的锁池中。
  2. 等待池: 假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程,调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。

如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

锁池和等待池切换原理:

1.如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

2.当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的线程便会进入该对象的锁池中,锁池中的线程会去重新竞争该对象锁。

3.优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

 

7 wait()和notify()实现原理

调用wait方法,即可进入等待池WaitSet,线程变为WAITING状态

BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片

BLOCKED线程会在Owner线程释放锁的时候被唤醒,并重新进入到锁池中,准备重新竞争锁

WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入锁池EntryList中重新竞争锁

锁池: 存放重试多次仍然没有获取到锁而阻塞的线程

等待池:存放显式调用wait 方法的线程----

锁池和等待池的相同点是都会阻塞,且都不占用CPU时间片

备注:

notify()----只会唤醒等待中的一个线程

notifyAll()-----唤醒所有的线程

 

 

原文链接:https://blog.csdn.net/qq_30056341/article/details/117715182



所属网站分类: 技术文章 > 博客

作者:天花灯

链接:http://www.javaheidong.com/blog/article/222383/e7c14bbf61741223f2d4/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

21 0
收藏该文
已收藏

评论内容:(最多支持255个字符)