synchronized解锁源码分析
上篇花了很大篇幅写了synchronized的加锁流程,并对比了ReentrantLock的设计,这篇我们收个尾,来聊一聊解锁流程,本来准备一章解决的,写着写着觉得内容过多,其实上一篇和ReentrantLock那篇结合起来都理解了,对锁的理解以及足够了,无论是公平锁,非公平锁,乐观锁,悲观锁,轻量锁,重量锁等等,基本可以融会贯通了,废话不多说,我们还是先看源码。
首先看入口在bytecodeInterpreter的CASE(_monitorexit):
CASE(_monitorexit): {
oop lockee = STACK_OBJECT(-1);
CHECK_NULL(lockee);
// derefing's lockee ought to provoke implicit null check
// find our monitor slot
// 和加锁流程一样的,拿到lock record栈顶和栈底,遍历栈中lock record
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
while (most_recent != limit ) {
if ((most_recent)->obj() == lockee) { //找到当前锁对应的lock record
BasicLock* lock = most_recent->lock();
markOop header = lock->displaced_header();
most_recent->set_obj(NULL); //回收
if (!lockee->mark()->has_bias_pattern()) { //偏向锁不做特殊操作,里面是轻量级锁和重量锁的退出操作
bool call_vm = UseHeavyMonitors;
// If it isn't recursive we either must swap old header or call the runtime
if (header != NULL || call_vm) { //轻量锁的第一个lock record或重量锁
markOop old_header = markOopDesc::encode(lock);
if (call_vm || lockee->cas_set_mark(header, old_header) != old_header) {
// restore object for the slow case
most_recent->set_obj(lockee);
CALL_VM(InterpreterRuntime::monitorexit(THREAD, most_recent), handle_exception);
}
}
}
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
}
most_recent++;
}
}
1.遍历所有当前栈钟lock record
2.找到指向锁对象的lock record
3.将lock record指向置为空,判断若为偏向锁不做处理
4.判断是轻量锁的第一个lock record或重量锁,将lock record再指回去 (有点秀啊,个人觉得写到最后一个if平级加一个else是不是会好点),进入下一步解锁流程
再往下看InterpreterRuntime::monitorexit:
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (elem == NULL || h_obj()->is_unlocked()) {
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
// Free entry. This must be done here, since a pending exception might be installed on
// exit. If it is not cleared, the exception handling code will try to unlock the monitor again.
elem->set_obj(NULL);
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) {
fast_exit(object, lock, THREAD);
}
void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
markOop mark = object->mark();
markOop dhw = lock->displaced_header();
if (dhw == NULL) { //重入场景,直接返回
return;
} if (mark == (markOop) lock) { //轻量锁场景,将原对象头mark word重置回去
if (object->cas_set_mark(dhw, mark) == mark) {
return;
}
} // We have to take the slow-path of possible inflation and then exit.
//重量级锁,膨胀,解锁
ObjectSynchronizer::inflate(THREAD,
object,
inflate_cause_vm_internal)->exit(true, THREAD);
}
1.锁重入直接返回。但是就synchronized解锁流程,感觉不会走啊
2.判断lock record是否指向锁对象mark word,是则为轻量级锁,将锁对象恢复为加锁前状态
3.走到这里肯定是重量级锁了,膨胀,解锁。
膨胀过程上一篇已经说过了,正常流程应该是膨胀完成直接退出了,我们直接看exit流程,我对比了jdk8和jdk12的,其实8的场景多一点而已,核心还是去队列中取一个作为候选线程去抢锁,这里直接介绍jdk12的吧:
void ObjectMonitor::exit(bool not_suspended, TRAPS) {
Thread * const Self = THREAD;
if (THREAD != _owner) {
if (THREAD->is_lock_owned((address) _owner)) { assert(_recursions == 0, "invariant");
_owner = THREAD;
_recursions = 0;
} else { return;
}
} if (_recursions != 0) {
_recursions--; // this is simple recursive enter
return;
} _Responsible = NULL; if (not_suspended && EventJavaMonitorEnter::is_enabled()) {
_previous_owner_tid = JFR_THREAD_ID(Self);
}
#endif for (;;) { OrderAccess::release_store(&_owner, (void*)NULL); // drop the lock
OrderAccess::storeload(); // See if we need to wake a successor
if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
return;
} if (!Atomic::replace_if_null(THREAD, &_owner)) {
return;
} guarantee(_owner == THREAD, "invariant"); ObjectWaiter * w = NULL; w = _EntryList;
if (w != NULL) { ExitEpilog(Self, w);
return;
} for (;;) {
assert(w != NULL, "Invariant");
ObjectWaiter * u = Atomic::cmpxchg((ObjectWaiter*)NULL, &_cxq, w);
if (u == w) break;
w = u;
} _EntryList = w;
ObjectWaiter * q = NULL;
ObjectWaiter * p;
for (p = w; p != NULL; p = p->_next) {
guarantee(p->TState == ObjectWaiter::TS_CXQ, "Invariant");
p->TState = ObjectWaiter::TS_ENTER;
p->_prev = q;
q = p;
}
if (_succ != NULL) continue; w = _EntryList;
if (w != NULL) {
guarantee(w->TState == ObjectWaiter::TS_ENTER, "invariant");
ExitEpilog(Self, w);
return;
}
}
}
这里首先会判断线程是否是自己,不是在判断线程是否由于轻量锁第一次加锁导致的变更,后面的其实就是先去_EntryList取线程节点,若为空,则去cxq取,我们主要看下释放锁具体做了什么:
void ObjectMonitor::ExitEpilog(Thread * Self, ObjectWaiter * Wakee) {
// Exit protocol:
// 1. ST _succ = wakee
// 2. membar #loadstore|#storestore;
// 2. ST _owner = NULL
// 3. unpark(wakee) _succ = Wakee->_thread; //将要释放的节点线程设为_success,这个参数其实在wait方法最后也涉及到了,一般来说就是下一个队列中要竞争锁的线程节点
ParkEvent * Trigger = Wakee->_event; Wakee = NULL; // Drop the lock
OrderAccess::release_store(&_owner, (void*)NULL);
OrderAccess::fence(); // ST _owner vs LD in unpark() DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self);
Trigger->unpark(); //unpark 线程,唤醒之后线程会继续在for循环中抢锁,抢不到继续park,这样无限循环,直到抢到锁,退出,执行代码块 OM_PERFDATA_OP(Parks, inc());
}
发现这里的释放锁其实和ReentrantLock的还是很相似的,用的也都是park和unpark,调用操作系统底层函数pthread_mutex_lock进入内核态。
关于锁的一些想法:
看完reentrantLock和synchronized之后,关于锁的理解也有了一些自己的心得,从操作系统层面来说,互斥锁就是pthread_mutex_lock函数,而对于java程序来说,锁也可以是一个状态,可以通过原子操作更改和判定这个状态来决定是否可以进入临界区内执行业务,只要能够达到控制多个线程在使用者的可控范围内执行的,都可以称之为锁,对于我们程序员来说,不用太过于在意锁是synchronized还是ReentrantLock甚至像读写锁之类的,其实都只是为了满足业务而诞生出来的一个工具,如synchronized就是为了解决并发场景下,不能有多个线程同时进来更改一个共享的值导致失控的场景,我理解的锁其实就是一个控制的工具,比如让我们来设计读写锁:
首先读锁和写锁不能同时进行,因为写的时候,读到的数据可能在一瞬间被改了,但是我们却不知道,而读与读之间却可以并发,看,多个不同线程同时访问一块代码,这也叫锁,而写与写之间不可以并发,因为并发场景你无法控制谁先谁后,你不知道最终会变成什么,这就失控了,像面试常会问到的锁升级,读锁是否可以升级为写锁呢,我们先考虑如果线程1获取了读锁,线程2也获取了读锁,线程1去获取写锁,这个时候会发现,线程2获取了读锁,而读写互斥,因此获取不到,线程2也是如此,上面我也说了,锁只是一个控制的工具,那我可以设计为能进去啊,但是这样还是会存在读写并存的问题,因此,从设计上来说,这种情况已经脱离掌控了,所以肯定是不可行的,而写锁降级为读锁为啥可行呢,因为写锁只有一个能进去,降级为读锁也就是释放了锁而已,而且在写锁释放之前,是没有线程可以获取到锁的,因此不可能出现读写锁并存的场景。
关于公平锁和非公平锁,其实就是一个线程进来的时候先去排队呢,还是先去抢锁,抢完在排队,由此可见,synchronized是一个妥妥的非公平锁嘛。
关于独占锁和共享锁,这个就更简单了,独占就是一个线程进去了,其他所有线程都不能入内,而共享锁则可以容纳允许范围内的线程,如CountDownLatch,由此可见synchronized是一个妥妥的独占锁,ReentrantLock也是。
关于锁的重入和不可重入,我们也看到了synchronized无论是偏向,轻量,重量级锁,都只是计数加一,明显是可重入的,而ReentrantLock也是用了一个aqs的state统计重入数量,妥妥的重入锁。
关于乐观和悲观锁,乐观锁是啥呢就是先让你试试,和预想的一样就修改成功(用cas或者数据库版本号做原子操作对比),不行在重来或加悲观锁,而悲观锁就是有人在操作,你就不许进来,等我改完再轮到你。
大体上我们常说的锁也就分为这些,尤其是乐观锁,充分的证明了我的观点,我压根不控制你,只要你和我预想的一样,也就是在我的掌控之内,就可以毫无阻拦,同时修改没有任何问题(当然也有可能出现ABA问题等但都是需要自己去衡量取舍的)。
总结:
写到这里,说实话结尾有点粗糙,但是总体想表达的想法还是写了出来,以前看过很多别人写的文章,背过关于上面的这些锁的概念应付面试,半个月,全部忘得干干净净,但是看了源码,跟着后面一步步去分析,去理解,确实收获很大,源码之路希望我能越走越远,读源码虽然枯燥,但是确实比看书,看别人写的东西要收获大的多,写这种博客说实话我也是花了不少勇气的,刚开始写的时候,其实自己真的有很多地方都没有理解,但是还是想试试,写的途中查的资料比写文章的时间多的多,写完之后,感觉自己的理解又加深了一截,身边的同事看到我在写这个,也准备回去看书了,哈哈,共同进步吧!!!
.tb_button { padding: 1px; cursor: pointer; border-right: 1px solid rgba(139, 139, 139, 1); border-left: 1px solid rgba(255, 255, 255, 1); border-bottom: 1px solid rgba(255, 255, 255, 1) }
.tb_button.hover { borer: 2px outset #def; background-color: rgba(248, 248, 248, 1) !important }
.ws_toolbar { z-index: 100000 }
.ws_toolbar .ws_tb_btn { cursor: pointer; border: 1px solid rgba(85, 85, 85, 1); padding: 3px }
.tb_highlight { background-color: rgba(255, 255, 0, 1) }
.tb_hide { visibility: hidden }
.ws_toolbar img { padding: 2px; margin: 0 }
synchronized解锁源码分析的更多相关文章
- ReentrantLock实现原理及源码分析
ReentrantLock是Java并发包中提供的一个可重入的互斥锁.ReentrantLock和synchronized在基本用法,行为语义上都是类似的,同样都具有可重入性.只不过相比原生的Sync ...
- synchronized的jvm源码分析聊锁的意义
上篇写完了ReentrantLock源码实现,从我们的角度分析设计锁,在对比大神的实现,顺道拍了一波道哥的马屁,虽然他看不到,哈哈.这一篇我们来聊一聊synchronized的源码实现,并对比reen ...
- JVM源码分析之synchronized实现
“365篇原创计划”第十二篇. 今天呢!灯塔君跟大家讲: JVM源码分析之synchronized实现 java内部锁synchronized的出现,为多线程的并发执行提供了一个稳定的 ...
- 集合操作出现的ConcurrentModificationException(源码分析)
摘要: 为了保证线程安全,在迭代器迭代的过程中,线程是不能对集合本身进行操作(修改,删除,增加)的,否则会抛出ConcurrentModificationException的异常. 示例: publi ...
- Java显式锁学习总结之六:Condition源码分析
概述 先来回顾一下java中的等待/通知机制 我们有时会遇到这样的场景:线程A执行到某个点的时候,因为某个条件condition不满足,需要线程A暂停:等到线程B修改了条件condition,使con ...
- ConcurrenHashMap源码分析(二)
本篇博客的目录: 一:put方法源码 二:get方法源码 三:rehash的过程 四:总结 一:put方法的源码 首先,我们来看一下segment内部类中put方法的源码,这个方法它是segment片 ...
- Java线程池ThreadPoolExector的源码分析
前言:线程是我们在学习java过程中非常重要的也是绕不开的一个知识点,它的重要程度可以说是java的核心之一,线程具有不可轻视的作用,对于我们提高程序的运行效率.压榨CPU处理能力.多条线路同时运行等 ...
- ReentrantLock 与 AQS 源码分析
ReentrantLock 与 AQS 源码分析 1. 基本结构 重入锁 ReetrantLock,JDK 1.5新增的类,作用与synchronized关键字相当,但比synchronized ...
- (转)ReentrantLock实现原理及源码分析
背景:ReetrantLock底层是基于AQS实现的(CAS+CHL),有公平和非公平两种区别. 这种底层机制,很有必要通过跟踪源码来进行分析. 参考 ReentrantLock实现原理及源码分析 源 ...
- Java并发编程 ReentrantLock 源码分析
ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大. 这个类主要基于AQS(Abst ...
随机推荐
- centos7安装桌面-GNOME
CENTOS7安装桌面系统 GNOME桌面 # yum安装 # 更新已安装软件 yum upgrade -y # 安装额外yum源 yum install epel-release -y # 安装X ...
- Task Manager 的设计简述
讲解 Task Manager 之前,在这里先介绍一些 Task Manager 会使用到的概念术语. 图数据库 Nebula Graph 中,存在一些长期在后台运行的任务,我们称之为 Job.存储层 ...
- Android 安装手机程序有问题/点击runAPP 程序安装不了手机
可以在 gradle.properties 里添加 android.injected.testOnly=false 点击同步 就可以运行了 如下:
- C++ //常用集合算法 //set_intersection //求俩个容器的交集 //set_union //求两个容器的并集 //set_difference //求两个容器的差集
1 //常用集合算法 2 //set_intersection //求俩个容器的交集 3 //set_union //求两个容器的并集 4 //set_difference //求两个容器的差集 5 ...
- 使用 Docker 部署 Answer 问答平台
1)介绍 GitHub:https://github.com/apache/incubator-answer Answer 问答社区是在线平台,让用户提出问题并获得回答.用户可以发布问题并得到其他用户 ...
- golang开发:环境篇(三)开发利器Goland安装
这节主要介绍下golang开发的最主要的IDE,Goland.可以有效提高开发效率.用过一段时间 IntelliJ+GO插件,其实功能上跟goland差不多.不过团队的其它开发者基本都是Goland, ...
- Windows 安装 Rust 并设置镜像加速
目录 下载rustup-init.exe(Rust安装工具) 使用镜像加速rustup安装 安装Rust 安装标准库源码 使用镜像加速cargo包下载 安装结果确认 更新.卸载和文档查看 参考文档 下 ...
- 基于Apollo3 Blue MCU芯片的可穿戴产品解决方案开发之六轴加速度传感器适配
一 前记 MPU-60X0 是全球首例9 轴运动处理传感器.它集成了3 轴MEMS 陀螺仪,3 轴MEMS加速度计,以及一个可扩展的数字运动处理器DMP(Digital Motion Processo ...
- vscode自动生成头文件
Ctrl Shift P 输入:snipp,选配置用户代码片段,新建全局代码片段文件,修改下列模板: { // Place your 全局 snippets here. Each snippet is ...
- 面试官:小伙子,能聊明白JMM给你SSP!我:嘚吧嘚吧一万字,直接征服面试官!
写在开头 面试官:小伙子,JMM了解吗? 我:JMM(Java Memory Model),Java内存模型呀,学过的! 面试官:那能给我详细的聊一聊吗,越详细越好! 我:嗯~,确定越详细越好?起码得 ...