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问题等但都是需要自己去衡量取舍的)。
总结:
写到这里,说实话结尾有点粗糙,但是总体想表达的想法还是写了出来,以前看过很多别人写的文章,背过关于上面的这些锁的概念应付面试,半个月,全部忘得干干净净,但是看了源码,跟着后面一步步去分析,去理解,确实收获很大,源码之路希望我能越走越远,读源码虽然枯燥,但是确实比看书,看别人写的东西要收获大的多,写这种博客说实话我也是花了不少勇气的,刚开始写的时候,其实自己真的有很多地方都没有理解,但是还是想试试,写的途中查的资料比写文章的时间多的多,写完之后,感觉自己的理解又加深了一截,身边的同事看到我在写这个,也准备回去看书了,哈哈,共同进步吧!!!