【并发编程】synchronized底层原理:Monitor(管程/监视器)

本文核心点

  • synchronized是非公平的锁!
  • 有线程在执行,新进入的线程会进入这个cxq这个队列中!
  • 本文释放锁分析使用的是默认策略(QMode=0):如果EntryList为空,则将cxq中的元素按原有顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取锁。_EntryList不为空,直接从_EntryList中唤醒线程。

synchronized到底是什么?

  • synchronized是JVM内置锁,基于Monitor机制实现。
  • 依赖底层操作系统的互斥原语Mutex(互斥量)。
  • 表面上它是一个重量级锁,性能较低。
  • 实际上JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。

Monitor(管程/监视器)

  • Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。
  • 管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。
  • synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。

管程模型

  • 在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。
  • 现在正在广泛使用的是MESA模型。

MESA模型.png

wait()、notify()和notifyAll()的使用

  • 使用wait有个范式要求:while(条件不满足)
  • 所有等待线程拥有相同的等待条件:使用notify()。
  • 所有等待线程被唤醒后,执行相同的操作:使用notify()。
  • 只需要唤醒一个线程:使用notify()。
  • 其他时候尽量使用notifyAll()。

Java内置的管程:synchronized

Java内置的管程:synchronized.png

  • Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。
  • MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。

Monitor机制在Java中的实现

  • java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法
  • wait(),notify(),notifyAll()的具体实现,依赖于 ObjectMonitor(JVM内部的机制) 实现。

ObjectMonitor的主要数据结构

    _header       = NULL; //对象头  markOop
    _count        = 0;  
    _waiters      = 0,   
    _recursions   = 0;   // synchronized是一个重入锁,这个变量记录锁的重入次数 
    _object       = NULL;  //存储锁对象
    _owner        = NULL;  // 标识拥有该monitor的线程(当前获取锁的线程) 
    _WaitSet      = NULL;  // 调用wait阻塞的线程:等待线程组成的双向循环链表,_WaitSet是第一个节点
    _WaitSetLock  = 0 ;    
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; // 有线程在执行,新进入的线程会进入这个队列:多线程竞争锁会先存到这个单向链表中 (FILO栈结构:非公平!)
    FreeNext      = NULL ;
    _EntryList    = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;

synchronized的等待唤醒机制

synchronized的等待唤醒机制.png

  • 在获取锁时,是将当前线程插入到cxq的头部。
  • 在释放锁时默认策略(QMode=0):如果EntryList为空,则将cxq中的元素按原有顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取锁。_EntryList不为空,直接从_EntryList中唤醒线程。

synchronized下线程的执行流程:等待机制!

  • 看一段代码的执行结果
public class SyncQModeDemo {

	public static void main(String[] args) throws InterruptedException {

		SyncQModeDemo demo = new SyncQModeDemo();

		demo.startThreadA();
		// 控制线程执行时间
		Thread.sleep(100);
		demo.startThreadB();
		Thread.sleep(100);
		demo.startThreadC();
	}

	final Object lock = new Object();

	public void startThreadA() {
		new Thread(() -> {
			synchronized (lock) {
				log.debug("A get lock");
				try {
					lock.wait(300);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				log.debug("A release lock");
			}
		}, "thread-A").start();
	}

	public void startThreadB() {
		new Thread(() -> {
			synchronized (lock) {
				try {
					log.debug("B get lock");
					Thread.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				log.debug("B release lock");
			}
		}, "thread-B").start();
	}

	public void startThreadC() {
		new Thread(() -> {
			synchronized (lock) {

				log.debug("C get lock");
			}
		}, "thread-C").start();
	}
}

执行结果

  • A get lock
  • B get lock
  • B release lock
  • A release lock
  • C get lock

为什么是这样的结果?

  • 第一个线程正常执行:owner是第一个线程!
  • 第二个线程进来,由于第一个在执行,他会阻塞:owner是第一个线程,cxq有第二个线程!
  • 假设这时候线程一调用wait()方法:WaitSet有第一个线程,cxq有第二个线程!owner为空!
  • 下一次进行争抢线程的使用权,EntryList是空的,cxq中的第线程去执行:WaitSet有第一个线程,owner是第二个线程!
  • 这时候第三个线程进来:cxq有第三个线程,WaitSet有第一个线程,cxq有第三个线程!owner是第二个线程!
  • 第二个线程执行完毕,唤醒其他线程,将WaitSet中的线程转移到EntryList:EntryList有第一个线程,cxq有第三个线程!
  • 下一次进行争抢线程的使用权,EntryList有值,直接从EntryList里面唤醒线程:EntryList有第一个线程,owner是第一个线程!
  • 第一个线程执行完毕,唤醒线程,只有cxq里面有线程,唤醒他:owner是第三个线程。

synchronized下线程的执行流程:竞争机制

  • 看一段代码的执行结果
public class SyncQModeDemo {

	public static void main(String[] args) throws InterruptedException {

		SyncQModeDemo demo = new SyncQModeDemo();

		demo.startThreadA();
		// 控制线程执行时间
		Thread.sleep(100);
		demo.startThreadB();
		Thread.sleep(100);
		demo.startThreadC();
	}

	final Object lock = new Object();

	public void startThreadA() {
		new Thread(() -> {
			synchronized (lock) {
				log.debug("A get lock");
				try {
					Thread.sleep(300);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				log.debug("A release lock");
			}
		}, "thread-A").start();
	}

	public void startThreadB() {
		new Thread(() -> {
			synchronized (lock) {
				try {
					log.debug("B get lock");
					Thread.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				log.debug("B release lock");
			}
		}, "thread-B").start();
	}

	public void startThreadC() {
		new Thread(() -> {
			synchronized (lock) {

				log.debug("C get lock");
			}
		}, "thread-C").start();
	}
}

执行结果

  • A get lock
  • A release lock
  • C get lock
  • B get lock
  • B release lock

为什么是这样的结果?

  • 第一个线程正常执行:owner是第一个线程!
  • 第一个线程没执行完,第二个线程进来:owner是第一个线程!cxq中有第二个线程!
  • 第一个线程没执行完,第三个线程进来:owner是第一个线程!cxq中有第二个线程,第三个线程(队列结构,第二个线程先进来,第三个线程后进来)!
  • 第一个线程执行完,原顺序从cxq中转移线程到EntryList:EntryList中有第二个线程,第三个线程(队列结构,第二个线程先进来,第三个线程后进来)!
  • 唤醒线程:owner是第三个线程!cxq中有第二个线程!
  • 线程三执行完:唤醒第二个线程,执行!

结束语

  • 获取更多有价值的文章,让我们一起成为架构师!
  • 关注公众号,可以让你对MySQL有非常深入的了解
  • 关注公众号,每天持续高效的了解并发编程!
  • 这个公众号,无广告!!!每日更新!!!
    作者公众号.jpg
posted @ 2022-01-17 01:12  程序java圈  阅读(752)  评论(0编辑  收藏  举报