前言:虽然我们使用多线程从而提高业务的效率,但是随之而来的就是多线程对于临界资源访问的安全问题;如对于同一个文档,多个线程同时进行写入,那么最终呈现出来的文档就很可能是错误的;
1 synchronized诞生的背景:
随着系统业务量的复杂,使用多线程可以更快的处理数据,更好的呈现给到客户结果,显然放弃多线程是不现实的,那么就只有通过对临界资源进行保护的方式,来避免多线程带来的安全问题,好在 java 中已经提供synchronized来解决多线程并发的问题;
2 synchronized 的使用:
java 中对于synchronized 的使用,传送门
3 synchronized 原理:
3.1 synchronized 之对象头:
我们知道synchronized 通过对对象的加锁,从而使得在同一时间内,只用一个线程可以对临界资源得访问,只有在一个线程使用完毕释放锁后,其余等待的线程才能去抢占锁,从而得到对临界资源的访问权限;
既然是锁,那么在java 里锁数据怎么保存呢?
对象在hotspot 中内存中的分配可以分 为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象的实际数据和对齐填充,并没有关于锁的踪迹,其中对象头Mark Word:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等(以32位jvm 为例)
3.2 锁升级过程:
在出现锁竞争时,jvm 并不一上来就直接挂起没有获得锁的线程,而是通过一系列的操作不断的尝试获取锁,从而尽可能的避免挂起当前线程(因为如果一旦挂起了改线程,就肯定回造成线程的上下文切换);
jvm 中偏向锁在jdk15以后默认是关闭的;所以有线程竞争时会直接到轻量级锁;当我们开启开启偏向锁后(使用使用XX:+UseBiasedLocking 开启);
锁的升级和撤销流程:
线程1 进入,发现此时无锁,通过CAS 操作将线程1的id 设置到对象头中,并且将锁的标记设置为偏向锁;
在线程1 还未执行完临界资源的访问时,线程2此时进入,进入后发现为偏向锁,则通过CAS操作 将线程2的id 设置到对象头中;CAS 成果则获取到锁,如果CAS 失败,则进行偏向锁的撤销;
偏向锁的撤销:
首先jvm会暂停所有线程的执行,然后检查当前对象头中记录的线程ID所指向的线程是否存活、或者是否执行完同步代码,如果执行完了,说明此时偏向锁处于无锁状态,将会重偏向给发起偏向锁撤销的那个线程。如果记录的线程ID仍然存活着,那么将偏向锁升级为轻量级锁。
偏向锁升级为轻量级锁加锁过程:
当前持有锁的线程,线程1在当前栈帧中开辟一块空间,保存对象markword的副本,并且修改对象头的锁状态,并且将对象头的Markword设置为指向该副本的指针;
线程2 则通过CAS 操作将对象头的Markword设置为指向该帧中保存的markword副本,如果设置成功,则说明成功获取锁,如果获取失败,则自旋等待获取锁(当自旋多次依旧没有获取到锁时。会将轻量级锁升级为重量级锁(设置对象的锁标记为重量级锁))。
轻量级锁升级为重量级锁加锁过程:
将对象的锁标识修改为重量级锁;生成一个Monitor 对象监视器,将对象头的Markword设置为指向改Monitor 对象的指针;线程2 则通过CAS 操作将与自己相关信息的Markword设置为指向改Monitor 对象的指针,设置成功则获取锁,设置失败则将当前线程放入到一个阻塞队列中并且挂起当前线程,让出CPU资源;当线程1释放Monitor 对象监视器后,唤醒阻塞队列中的线程重新去通过CAS 操作抢占Monitor 对象监视器;
4 synchronized 加锁解锁部分源码:
4.1 偏向锁和轻量级锁的获取:
CASE(_monitorenter): {
// 获取锁对象
oop lockee = STACK_OBJECT(-1);
// 在线程栈上找到一个空闲的BasicObjectLock对象
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
BasicObjectLock* entry = NULL;
while (most_recent != limit ) {
if (most_recent->obj() == NULL) entry = most_recent;
else if (most_recent->obj() == lockee) break;
most_recent++;
}
if (entry != NULL) {
// 保存锁对象,表明当前BasicObjectLock持有锁对象lockee
entry->set_obj(lockee);
int success = false;
uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;
markOop mark = lockee->mark(); // 获取锁对象的头部标记信息
// 获取没有hash值的标记位值,这里为0
intptr_t hash = (intptr_t) markOopDesc::no_hash;
// 判断使用了偏向锁
if (mark->has_bias_pattern()) {
uintptr_t thread_ident;
uintptr_t anticipated_bias_locking_value;
thread_ident = (uintptr_t)istate->thread(); // 获取线程id
anticipated_bias_locking_value =
(((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
~((uintptr_t) markOopDesc::age_mask_in_place);
/* anticipated_bias_locking_value为0,表明还没有批量撤销偏向锁,且当前线程
持有了偏向锁,直接退出 */
if (anticipated_bias_locking_value == 0) {
// already biased towards this thread, nothing to do
if (PrintBiasedLockingStatistics) {
(* BiasedLocking::biased_lock_entry_count_addr())++;
}
success = true;
}
else if ((anticipated_bias_locking_value &
markOopDesc::biased_lock_mask_in_place) != 0) {
/* anticipated_bias_locking_value不为0,可能是批量撤销偏向锁,需要继续判断是否有
线程持有偏向锁,如果其他线程持有偏向锁,判定发生了冲突,就需要撤销偏向锁 */
markOop header = lockee->klass()->prototype_header();
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
// CAS将对象头从mark替换为header撤销偏向锁
if (lockee->cas_set_mark(header, mark) == mark) {
if (PrintBiasedLockingStatistics)
(*BiasedLocking::revoked_lock_entry_count_addr())++;
}
}
else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
/* 如果anticipated_bias_locking_value不为0,在批量撤销偏向锁时需要更改
epoch的值,这里如果epoch改变了,当前线程需要重偏向 */
markOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);
if (hash != markOopDesc::no_hash) {
new_header = new_header->copy_set_hash(hash);
}
// CAS重偏向
if (lockee->cas_set_mark(new_header, mark) == mark) {
if (PrintBiasedLockingStatistics)
(* BiasedLocking::rebiased_lock_entry_count_addr())++;
}
else {
// CAS失败,发生了竞争,那么进入monitorenter
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;
}
else {
/* 以上条件均不满足,表明开启了偏向锁,此时偏向锁状态为匿名偏向,尝试CAS
将其偏向为当前线程*/
markOop header = (markOop) ((uintptr_t) mark &
((uintptr_t)markOopDesc::biased_lock_mask_in_place |
(uintptr_t)markOopDesc::age_mask_in_place |
epoch_mask_in_place));
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
markOop new_header = (markOop) ((uintptr_t) header | thread_ident);
// CAS重偏向
if (lockee->cas_set_mark(new_header, header) == header) {
if (PrintBiasedLockingStatistics)
(* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
}
else {
// CAS失败,发生了竞争,那么进入monitorenter
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry),
handle_exception);
}
success = true;
}
}
// 没有获取到锁,那么进入传统的轻量级锁
if (!success) {
markOop displaced = lockee->mark()->set_unlocked();
entry->lock()->set_displaced_header(displaced);
bool call_vm = UseHeavyMonitors; // 判断是否直接使用重量级锁
/* 如果没有指定直接使用重量级锁,那么通过CAS操作尝试获取轻量级锁,即替换
头部指针,指向entry */
if (call_vm || lockee->cas_set_mark((markOop)entry, displaced) != displaced) {
// 如果失败,可能是当前线程轻量级锁重入,那么判断是否是锁重入
if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits()))
{
// 轻量级锁重入,不需要设置displaced_header信息
entry->lock()->set_displaced_header(NULL);
} else {
// 否则调用monitorenter
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry),
handle_exception);
}
}
}
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
} else {
// 如果未找到,设置more_monitors标志位,由解释器分配新的BasicObjectLock并重试
istate->set_msg(more_monitors);
UPDATE_PC_AND_RETURN(0); // Re-execute
}
}
流程:
1 如果没有禁用偏向锁,会尝试构造一个偏向当前线程的mark word,使用cas进行替换,替换成功则获取锁;失败则执行偏向锁的撤销,然后由偏向锁膨胀为轻量级锁;
2 如果禁用了偏向锁构造无锁mark word,构造的lock record指向该无锁mark word,然后进行轻量级锁加锁;加锁成功获取锁,失败后在经过挣扎后膨胀为重量级锁;
4.2 重量级锁的获取:
void ATTR ObjectMonitor::EnterI (TRAPS) {
Thread * Self = THREAD ;
// 开始之前先尝试获取锁
if (TryLock (Self) > 0) {
return ;
}
DeferredInitialize () ; // 初始化监视器,这个不影响流程,一会儿讲解
/* 先尝试自旋,还记得之前在调用这个方法时有个判断吗?if (Knob_SpinEarly && TrySpin (Self) > 0),这边的开关就是Knob_SpinEarly是否允许提前自旋,默认是1 */
if (TrySpin (Self) > 0) {
return ;
}
// 自旋失败,那么开始进入等待队列,并且开始阻塞线程
// 把当前线程放入监视器的_cxq竞争队列中
// 队列节点充当当前线程的代理,熟悉AQS的读者,可以发现这里有异曲同工之妙
ObjectWaiter node(Self) ; // 创建等待节点
Self->_ParkEvent->reset() ; // 初始化ParkEvent
node._prev = (ObjectWaiter *) 0xBAD ; // 设置prev节点为BAD地址,即一个空对象
// 设置节点状态为CXQ状态,表明放入_cxq队列
node.TState = ObjectWaiter::TS_CXQ ;
// 将线程push到cxq的头部
// 一旦线程处于cxq或者entrylist上,线程就会一直在队列上,直到它获得了监视器的锁
ObjectWaiter * nxt ;
for (;;) { // 死循环常规操作
node._next = nxt = _cxq ; // 设置node的next为_cxq,即头插法,每次插在头部
// CAS插入,如果插入成功直接退出循环
if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
// CAS失败,表明有线程竞争成功,这里通过优化尝试获取锁,如果不行,则继续CAS
if (TryLock (Self) > 0) {
return ;
}
}
/* 当nxt节点为空且_EntryList也为空,表明当前只有这个线程正在等待锁,因为nxt是跟着
_cxq走的,想象一下,如果这个线程把自己插入了头部,即_cxq就等于这个线程,
但是nxt为null表明后面没有线程了,谁来唤醒这个线程呢?所以这里检测边界条件 */
if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
// CAS尝试将当前线程作为线程_Responsible角色
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}
int nWakeups = 0 ;
int RecheckInterval = 1 ;
for (;;) { // 再次进入死循环,直到获得锁才会退出循环
if (TryLock (Self) > 0) break ; // 先尝试获取锁
// _owner不能为当前线程,因为上面都获取失败了,为什么监视器持有对象会是这个线程
if ((SyncFlags & 2) && _Responsible == NULL) {
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}
// 将线程park阻塞
// 如果_Responsible为当前线程且SyncFlags最后一位为1,这个参数默认为0
if (_Responsible == Self || (SyncFlags & 1)) {
// 调用ParkEvent 阻塞RecheckInterval时间
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
RecheckInterval *= 8 ; // 逐渐增加park时间
if (RecheckInterval > 1000) RecheckInterval = 1000 ; // 最大为1000ms
} else {
// 到这一步表明后面有等待线程会负责通知自己,直接永久park阻塞,直到被唤醒
Self->_ParkEvent->park() ;
}
if (TryLock(Self) > 0) break ; // 唤醒后尝试获取锁
// 到这一步,锁仍然被争用,只需要记录下无用的唤醒次数即可
if (ObjectMonitor::_sync_FutileWakeups != NULL) {
ObjectMonitor::_sync_FutileWakeups->inc() ;
}
++ nWakeups ;
// 如果开启了Knob_SpinAfterFutile,即被无用唤醒后是否自旋
if ((Knob_SpinAfterFutile & 1) && TrySpin (Self) > 0) break ;
// 这个参数默认关闭,不用管,也就是判断是否重置ParkEvent
if ((Knob_ResetEvent & 1) && Self->_ParkEvent->fired()) {
Self->_ParkEvent->reset() ;
OrderAccess::fence() ;
}
// 如果_succ等于当前线程,那么赋值为NULL
if (_succ == Self) _succ = NULL ;
// 清空_succ后线程必须重试获取锁,所以又用了全屏障来保证指令顺序
// 继续循环获取锁
OrderAccess::fence() ;
}
// 这一步表明线程获得了监视器的锁
UnlinkAfterAcquire (Self, &node) ; // 获取锁后断开链表
if (_succ == Self) _succ = NULL ;
if (_Responsible == Self) {
_Responsible = NULL ;
OrderAccess::fence();
}
if (SyncFlags & 8) {
OrderAccess::fence() ;
}
return ;
}
加锁流程图解:
当获取锁失败时,构建一个等待节点ObjectWaiter 并且使用头插法插入到_cxq 的队列上;然后使用park()挂起当前线程;
4.3 重量级锁的解锁:
#ObjectMonitor.cpp
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
Thread * Self = THREAD ;
//释放锁的线程不一定是重量级锁的获得者-------->(1)
if (THREAD != _owner) {
if (THREAD->is_lock_owned((address) _owner)) {
//释放锁的线程是轻量级锁的获得者,先占用锁
_owner = THREAD ;
} else {
//异常情况
return;
}
}
if (_recursions != 0) {
//是重入锁,简单标记后退出
_recursions--;
return ;
}
...
for (;;) {
if (Knob_ExitPolicy == 0) {
//默认走这里
//释放锁,别的线程可以抢占了
OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lock
OrderAccess::storeload() ; // See if we need to wake a successor
//如果没有线程在_cxq/_EntryList等待,则直接退出
if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
TEVENT (Inflated exit - simple egress) ;
return ;
}
//有线程在等待,再把之前释放的锁拿回来
if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
//若是失败,说明被人抢占了,直接退出
return ;
}
} else {
...
}
ObjectWaiter * w = NULL ;
int QMode = Knob_QMode ;
//此处省略代码
//根据QMode不同,选不同的策略,主要是操作_cxq和_EntryList的方式不同
//默认QMode=0
w = _EntryList ;
if (w != NULL) {
//_EntryList不为空,则释放锁---------(2)
ExitEpilog (Self, w) ;
return ;
}
//_EntryList 为空,则看_cxq有没有数据
w = _cxq ;
if (w == NULL) continue ;//没有继续循环
for (;;) {
//将_cxq头节点置空
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
if (u == w) break ;
w = u ;
}
if (QMode == 1) {
...
} else {
// QMode == 0 or QMode == 2
//_EntryList指向_cxq
_EntryList = w ;
ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
//该循环的目的是为了将_EntryList里的节点前驱连接起来---------(3)
for (p = w ; p != NULL ; p = p->_next) {
guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
//改为ENTER状态
p->TState = ObjectWaiter::TS_ENTER ;
p->_prev = q ;
q = p ;
}
}
w = _EntryList ;
if (w != NULL) {
//释放锁---------(4)
ExitEpilog (Self, w) ;
return ;
}
}
}
// 根据QMode不同,选不同的策略,主要是操作_cxq和_EntryList的方式不同
重量级锁解锁图解:
ExitEpilog (Self, w) 释放锁:
void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {
//从队列节点里取出ParkEvent
ParkEvent * Trigger = Wakee->_event ;
Wakee = NULL ;
//释放锁,将_owner置空
OrderAccess::release_store_ptr (&_owner, NULL) ;
OrderAccess::fence() ; // ST _owner vs LD in unpark()
//唤醒节点里封装的线程
Trigger->unpark() ;
}
从释放锁的流程已经得知:当前占有锁的线程释放锁后会唤醒阻塞等待锁的线程
具体唤醒哪个线程,要看QMode值,以默认值QMode=0为例:
1、若是_EntryList队列不为空,则取出_EntryList队头节点并唤醒。
2、若是_EntryList为空,将_EntryList指向_cxq,并取出队头节点唤醒。
4.4 重量级锁的_waitSet源码:
重量级锁的_waitSet源码:包装当前线程ObjectWait,状态会设置为TS_WAIT,然后将它插入到_waitSet中(一个双向链表)
在根据不同的策略插入到_cxq 还是EntryList:
5 synchronized 总结 加锁和释放锁流程:
图上流程对应的场景如下:
1、线程A先抢占锁,A在进入阻塞队列前已经成功获取锁。
2、而后线程B抢占锁,发现锁已被占有,于是加入阻塞队列队头。
3、最后线程C也来抢占锁,发现锁已经被占有,于是加入阻塞队列队头,此时B已经被C抢了队头位置。
4、当A释放锁后,唤醒阻塞队列里的队头线程C,C开始去抢占锁。
5、C拿到锁后,将自己从阻塞队列里移出。
6、后面的流程和之前一样。
扩展:
1 开启偏向锁后当对象从无锁升级为偏向锁,我们发现这个时候对象的hashCode 丢失了;这个时候如果我们调用hashCode 锁会直接变成重量级锁;
轻量级锁的hashCode 时保存在线程的栈帧中;
重量级锁的hashCode 是保存则对象监听器中;
2 :可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的;
3 :锁消除:
锁消除是指虚拟机即时编译器在运行时编译器在运行时检测到某段需要同步的代码块根本不可能存在共享数据竞争而实施的一种对锁进行消除的优化策略。锁消除的主要判定依据来源于逃逸分析,逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写就不会有竞争,对这个变量实施的同步措施也就可以安全的消除。
逃逸分析 :逃逸分析并不是100%准确。
hospot在1.6之后才开始支持逃逸分析,至今这项技术还未完全成熟,不成熟的主要原因是逃逸分析的计算成本非常高,甚至无法保证逃逸分析的性能收益会高于他的消耗。(现如今Java语言在服务逐渐小型化的大趋势中已经显得略有笨重,主要的劣势来自于即时编译、提前编译,这种大压力的算法正是即时编译的弱项)
所以目前虚拟机采用的是并不那么精准,但时间压力相对较小的算法来处理逃逸分析。
4 锁粗化:
试想一下,如果有一系列的操作都对同一个对象反复加锁和解锁,甚至在一个循环体中,那此即使没有线程竞争,频繁的进行互斥操作也会导致不必要的性能开销。
如果虚拟机探测到这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围粗化(扩展)到整个操作序列外部。
参考:
https://zhuanlan.zhihu.com/p/440994983
https://blog.youkuaiyun.com/qq_34677946/article/details/123609833
https://blog.youkuaiyun.com/u013643074/article/details/125596328
https://www.jianshu.com/p/be4ef14e123c