gpt4 book ai didi

09.什么是synchronized的重量级锁?

转载 作者:我是一只小鸟 更新时间:2023-01-09 14:31:38 28 4
gpt4 key购买 nike

大家好,我是王有志。关注 王有志 ,一起聊技术,聊游戏,聊在外漂泊的生活.

今天我们继续学习 synchronized 的升级过程,目前只剩下最后一步了:轻量级锁->重量级锁.

通过今天的内容,希望能帮助大家解答 synchronized都问啥? 中除锁粗化,锁消除以及Java 8对 synchronized 的优化外全部的问题.

获取重量级锁

从源码揭秘偏向锁的升级 最后,看到 synchronizer#slow_enter 如果存在竞争,会调用 ObjectSynchronizer::inflate 方法,进行轻量级锁的升级(膨胀).

Tips :

                        
                          void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
	......
	ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
}

                        
                      

通过 ObjectSynchronizer::inflate 获取重量级锁ObjectMonitor,然后执行 ObjectMonitor::enter 方法.

Tips :

  • 关于线程你必须知道的8个问题(中) 中提到过该方法;
  • 问题是锁升级(膨胀),但重点不在 ObjectSynchronizer::inflate ,因此代码分析放在 重量级锁源码分析 中。

锁的结构

了解 ObjectMonitor::enter 的逻辑前,先来看 ObjectMonitor 的结构:

                        
                          class ObjectMonitor {
	private:
		// 保存与ObjectMonitor关联Object的markOop
		volatile markOop   _header;

		// 与ObjectMonitor关联的Object
		void*     volatile _object;
	protected:

		// ObjectMonitor的拥有者
		void *  volatile _owner;
		
		// 递归计数
		volatile intptr_t  _recursions;

		// 等待线程队列,cxq移入/Object.notify唤醒的线程
		ObjectWaiter * volatile _EntryList;
	private:

		// 竞争队列
		ObjectWaiter * volatile _cxq;
		
		// ObjectMonitor的维护线程
		Thread * volatile _Responsible;
	protected:
	
		// 线程挂起队列(调用Object.wait)
		ObjectWaiter * volatile _WaitSet;
}

                        
                      

_header 字段存储了Object的markOop,为什么要这样?因为 锁升级后没有空间存储Object的markOop了,存储到_header中是为了在退出时能够恢复到加锁前的状态 .

Tips :

  • 实际上 basicLock 也存储了对象的markOop;
  • EntryList 中等待线程来自于 cxq 移入,或 Object.notify 唤醒但未执行。

重入的实现

objectMonito#enter 方法可以拆成三个部分,首先是 竞争成功或重入的场景 :

                        
                          // 获取当前线程Self
Thread * const Self = THREAD;

// CAS抢占锁,如果失败则返回_owner
void * cur = Atomic::cmpxchg(Self, &_owner, (void*)NULL);
if (cur == NULL) {
	// CAS抢占锁成功直接返回
	return;
}

// CAS失败场景
// 重量级锁重入
if (cur == Self) {
	// 递归计数+1
	_recursions++;
	return;
}

// 当前线程是否曾持有轻量级锁
// 可以看做是特殊的重入
if (Self->is_lock_owned ((address)cur)) {
	// 递归计数器置为1
	_recursions = 1;
	_owner = Self;
	return;
}

                        
                      

重入和升级的场景中,都会操作 _recursions 。 _recursions 记录了进入ObjectMonitor的次数,解锁时要经历相应次数的退出操作才能完成解锁.

适应性自旋

以上都是成功获取锁的场景,那么产生竞争导致失败的场景是怎样的呢?来看 适应性自旋 的部分, ObjectMonitor倒数第二次对“轻量”的追求 :

                        
                          // 尝试自旋来竞争锁
Self->_Stalled = intptr_t(this);
if (Knob_SpinEarly && TrySpin (Self) > 0) {
	Self->_Stalled = 0;
	return;
}

                        
                      

objectMonitor#TrySpin 方法是对 适应性自旋 的支持。Java 1.6后加入,移除默认次数的自旋,将自旋次数的决定权交给JVM.

JVM根据锁上一次自旋情况决定,如果刚刚自旋成功,并且持有锁的线程正在执行,JVM会允许再次尝试自旋。如果该锁的自旋经常失败,那么JVM会直接跳过自旋过程 .

Tips :

  • 适应性自旋的原码分析放在了 重量级锁源码分析 中;
  • objectMonitor#TryLock 非常简单,关键技术依旧是CAS。

互斥的实现

到目前为止,无论是CAS还是自旋,都是偏向锁和轻量级锁中出现过的技术,为什么会让ObjectMonitor背上“重量级”的名声呢?

最后是竞争失败的场景:

                        
                          // 此处省略了修改当前线程状态的代码
for (;;) {
	EnterI(THREAD);
}

                        
                      

实际上,进入 ObjectMonitor#EnterI 后也是先尝试“轻量级”的加锁方式:

                        
                          void ObjectMonitor::EnterI(TRAPS) {
	if (TryLock (Self) > 0) {
		return;
	}

	if (TrySpin (Self) > 0) {
		return;
	}
}

                        
                      

接来下是重量级的真正实现:

                        
                          // 将当前线程(Self)封装为ObjectWaiter的node
ObjectWaiter node(Self);
Self->_ParkEvent->reset();
node._prev   = (ObjectWaiter *) 0xBAD;
node.TState  = ObjectWaiter::TS_CXQ;

// 将node插入到cxq的头部
ObjectWaiter * nxt;
for (;;) {
	node._next = nxt = _cxq;
	if (Atomic::cmpxchg(&node, &_cxq, nxt) == nxt)
		break;

	// 为了减少插入到cxq头部的次数,试试能否直接获取到锁
	if (TryLock (Self) > 0) {
		return;
	}
}

                        
                      

逻辑一目了然,封装 ObjectWaiter 对象,并加入到 cxq 队列头部。接着往下执行:

                        
                          // 将当前线程(Self)设置为当前ObjectMonitor的维护线程(_Responsible)
// SyncFlags的默认值为0,可以通过-XX:SyncFlags设置
if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
	Atomic::replace_if_null(Self, &_Responsible);
}

for (;;) {
	// 尝试设置_Responsible
	if ((SyncFlags & 2) && _Responsible == NULL) {
		Atomic::replace_if_null(Self, &_Responsible);
	}
	// park当前线程
	if (_Responsible == Self || (SyncFlags & 1)) {
		Self->_ParkEvent->park((jlong) recheckInterval);	
		// 简单的退避算法,recheckInterval从1ms开始
		recheckInterval *= 8;
		if (recheckInterval > MAX_RECHECK_INTERVAL) {
			recheckInterval = MAX_RECHECK_INTERVAL;
		}
	} else {
		Self->_ParkEvent->park();
	}

	// 尝试获取锁
	if (TryLock(Self) > 0)
		break;
	if ((Knob_SpinAfterFutile & 1) && TrySpin(Self) > 0)  
	    break;

	if (_succ == Self)
		_succ = NULL;
}

                        
                      

逻辑也不复杂,不断的 park 当前线程,被唤醒后尝试获取锁。需要关注 -XX:SyncFlags 的设置:

  • SyncFlags == 0 时, synchronized 直接挂起线程;
  • SyncFlags == 1 时, synchronized 将线程挂起指定时间。

前者是永久挂起,需要被其它线程唤醒,而后者挂起指定的时间后自动唤醒 .

Tips : 关于线程你必须知道的8个问题(中) 聊到过 park 和 parkEvent ,底层是通过 pthread_cond_wait 和 pthread_cond_timedwait 实现的.

释放重量级锁

释放重量级锁的源码和注释非常长,我们省略大部分内容,只看关键部分.

重入锁退出

我们知道,重入是不断增加 _recursions 的计数,那么退出重入的场景就非常简单了:

                        
                          void ObjectMonitor::exit(bool not_suspended, TRAPS) {
	Thread * const Self = THREAD;

	// 第二次持有锁时,_recursions == 1
	// 重入场景只需要退出重入即可
	if (_recursions != 0) {
		_recursions--;
		return;
	}
	.....
}

                        
                      

不断的减少 _recursions 的计数.

释放和写入

JVM的实现中,当前线程是锁的持有者且没有重入时, 首先会释放自己持有的锁,接着将改动写入到内存中,最后还肩负着唤醒下一个线程的责任 。先来看释放和写入内存的逻辑:

                        
                          // 置空锁的持有者
OrderAccess::release_store(&_owner, (void*)NULL);

// storeload屏障,
OrderAccess::storeload();

// 没有竞争线程则直接退出
if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
	TEVENT(Inflated exit - simple egress);
	return;
}

                        
                      

storeload 屏障,对于如下语句:

                        
                          store1;
storeLoad;
load2

                        
                      

保证 store1 指令的写入在 load2 指令执行前,对所有处理器可见.

Tips : volatile 中详细解释内存屏障.

唤醒的策略

执行释放锁和写入内存后,只需要唤醒下一个线程来“交接”锁的使用权。但是有两个“等待队列”: cxq 和 EntryList ,该从哪个开始唤醒呢?

Java 11前,根据 QMode 来选择不同的策略:

  • QMode == 0 ,默认策略,将 cxq 放入 EntryList
  • QMode == 1 ,翻转 cxq ,并放入 EntryList
  • QMode == 2 ,直接从 cxq 中唤醒;
  • QMode == 3 ,将 cxq 移入到 EntryList 的尾部;
  • QMode == 4 ,将 cxq 移入到 EntryList 的头部。

不同的策略导致了不同的唤醒顺序,现在你知道为什么说 synchronized 是非公平锁了吧?

objectMonitor#ExitEpilog 方法就很简单了,调用的是与 park 对应的 unpark 方法,这里就不多说了.

Tips : Java 12的objectMonitor 移除了 QMode ,也就是说只有一种唤醒策略了.

总结

我们对重量级锁做个总结。 synchronized 的重量级锁是 ObjectMonitor ,它使用到的关键技术有 CAS和park 。相较于 mutex#Monitor 来说,它们的本质相同,对park的封装,但 ObjectMonitor 是做了大量优化的复杂实现.

我们看到了重量级锁是如何实现重入性的,以及唤醒策略导致的“不公平”。那么我们常说的 synchronized 保证了原子性,有序性和可见性,是如何实现的呢?

大家可以先思考下这个问题,下篇文章会做一个全方位的总结,给 synchronized 收下尾.


好了,今天就到这里了,Bye~~ 。

最后此篇关于09.什么是synchronized的重量级锁?的文章就讲到这里了,如果你想了解更多关于09.什么是synchronized的重量级锁?的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com