1同步锁synchronized追根溯源它也被认为是受灾最严重的地区。因为它不像其他代码,它是有源码的,synchronized是关键字,可以查看。我无法直接找到源代码。下面通过java内存指令代码和c++源码(HotSpot虚拟机源码)来分析synchronized是如何实现锁同步的。1.1同步场景复习目标:synchronized复习概念synchronized:是Java中的关键字,是一种同步锁。syn属于什么锁分类:乐观锁、悲观锁(syn)排他锁(syn)、共享锁公平锁、非公平锁(syn)互斥锁(syn)、读写锁可重入锁(syn)小技巧:synchronizedJDK1.6锁升级:无锁->偏向锁(非锁)->轻量级锁->重量级锁(1.6之前)多线程特性回顾(面试常问)原子性:指一个操作或多个操作,要么全部执行并且执行过程不会被任何因素打断,要么根本不执行。可见性:指多个线程访问一个资源时,状态、值信息等都是可见的。有序性:指代码在程序中的执行顺序(编译器会重新排列)sync可以充分实现以上三个特性保证线程安全,而cas不能实现原子性。原理是什么?1.2反汇编找锁实现原理目标通过javap反汇编看synchronized是怎么加锁的com.syn.BTestpublicclassBTest{privatestaticObjectobject=newObject();publicsynchronizedvoidtestMethod(){System.out.println("HelloWorld-同步方法");}publicstaticvoidmain(String[]args){synchronized(object){System.out.println("HelloWorld-synchronizedblock");}}}反汇编后,我们会看到什么JDK自带的工具:javap,对字节码进行反汇编://com.syn.BTestjavap-v-cBTest.class-v:输出附加信息-c:反汇编代码及反汇编后解释synchronized修改后的代码块多了两条指令monitorenter和monitorexit,即JVM使用monitorenter和monitorexit这两条指令来实现同步解释。调用方法时,会检查方法的ACC_SYNCHRONIZED访问标志是否设置。monitor,只有获取成功后才能执行方法体,方法执行完毕后释放monitor。即jvm会隐式调用monitorenter和monitorexit。monitorenter原理monitorenter首先我们来看一下JVM规范中对monitorenter的描述https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorentermonitorenter:每个对象都是与监视器相关联。当且仅当监视器有所有者时,它才会被锁定。执行monitorenter的线程试图获得与objectref关联的监视器的所有权,如下所示:?如果与objectref关联的监视器的条目计数为零,则线程进入监视器并将其条目计数设置为一。然后该线程成为监视器的所有者。?如果该线程已经拥有与objectref关联的监视器,它会重新进入监视器,增加其条目计数。?如果另一个线程已经拥有与objectref关联的监视器,则线程阻塞直到监视器的入口计数为零,然后再次尝试获得所有权。monitorexit: 执行monitorexit的线程必须是与引用的实例关联的monitor的所有者objectref.线程递减与objectref关联的监视器的条目计数。如果结果是条目计数的值为零,则线程退出监视器并且不再是它的所有者。其他阻塞进入监视器的线程被允许尝试这样做。翻译如下:monitorenter的每个对象都会关联一个monitor监视器。当monitor被占用时,会被锁住,其他线程无法获取monitor。JVM在线程的方法内部执行monitorenter时,会尝试获取当前对象对应的monitor的所有权。过程如下:如果monitorentry数为0,则线程可以进入monitor并将monitorentry数设置为1,当前线程成为monitor的所有者(owner)。如果线程已经拥有管程的所有权并允许其重新进入管程,则进入管程的次数加1。如果其他线程已经拥有管程的所有权,则当前试图获取管程的线程monitor的所有权它将被阻塞,直到monitor的条目计数变为0,然后再尝试获取monitor的所有权。monitorexit能够执行monitorexit命令的线程必须是拥有当前对象的monitor的线程。当执行monitorexit时,monitorentry的个数会减1,当monitorentry的个数减为0时,当前线程退出monitor,不再拥有monitor的所有权。这时其他被monitor阻塞的线程可以尝试获取monitormonitorexit释放锁的所有权。monitorexit被插入在方法的末尾和exception处,JVM保证每个monitorenter都必须有对应的monitorexit。tips(重要)简单理解,monitor是jvm底层c++代码中的一个对象ObjectMonitor。这个对象中有一个计数器,记录当前对象锁是否被使用过,使用过多少次。还有一些队列用来存储和调度一些需要这个锁的线程。关于C++中monitor的结构,我们下面会详细讨论。总结:1.Synchronized由ObjectMonitor控制。2.需要这个锁的线程在监视器中被安排在各个队列中。3.获得锁的线程被monitor标记,计数完毕,释放锁。减法操作1.3Monitor详解目标:Monitor的位置接下来,让我们看一下它的详细内部结构以及它是如何工作的。1.3.1Monitor的目标是什么:通过JVM虚拟机的源码来分析一下synchronizedmonitor。(说句实话)在HotSpot虚拟机中,monitor监控器是通过ObjectMonitor来实现的。构造函数代码src/share/vm/runtime/objectMonitor.hpphpp可以包含包含cpp的东西,两者都是c++代码//constructorObjectMonitor(){_header=NULL;_count=0;_waiters=0,_recursions=0;//线程重入次数_object=NULL;_owner=NULL;//当前线程,获得锁的线程_WaitSet=NULL;//等待队列,调整等待的线程在这里_WaitSetLock=0;_Responsible=NULL;_成功=空;_cxq=空;//竞争队列,无法获取到这里前进的锁(可以自旋)FreeNext=NULL;_EntryList=NULL;//阻塞队列,来自cxq(解锁时)或waitSet(调用notify时)_SpinFreq=0;_自旋时钟=0;OwnerIsThread=0;}注意这三个链表:1)cxq(竞争链表)cxq是一个单向链表。挂起线程等待重新竞争锁的链表,monitor通过CAS将包作为ObjectWaiter写入链表头部。为了避免插入和删除元素之间的竞争,Owner会从列表的末尾取元素。所以这个东西可以理解为上来竞争的时候没有拿到锁的暂时在这里停留一段时间(一级缓存)。2)EntryList(锁定候选列表)EntryList是一个双向链表。当EntryList为空且cxq不为空时,Owener会在解锁时将cxq中的数据移动到EntryList中。并指定EntryList头部的第一个线程为OnDeck线程,其他线程留在里面。所以这个东西可以认为是第二个没有拿到的争用锁(其中一个很快就会拿到)。(二级缓存)备注:EntryList和cxq的区别在于cxq中的队列可以不断自旋等待锁。如果达到自旋阈值后仍未获得锁,则会调用park方法挂起。EntryList中的线程都是挂起的线程。3)WaitListWatiList是Owner线程调用wait()方法后进入的线程。进入WaitList的线程会在调用notify()/notifyAll()后加入到EntryList中。流程总结:等待锁的线程会停留在_cxq和entryset队列中,这与当前线程锁情况有关。entrysetheader线程获取到对象的monitor后进入_Owner区域,将monitor中的_owner变量设置为自己,monitor中的计数器_count加1。如果线程调用wait()方法,当前持有的监视器会被释放,_owner变量恢复为null,_count会减1。同时线程会进入_WaitSet集合等待被唤醒。如果当前线程执行完毕,也会释放管程(锁)并重置变量的值,以便其他线程进入并获取管程(锁)。1.3.2详细流程图(了解)monitorentermonitorenter指令执行位置:JVM源码:src/share/vm/interpreter/interpreterRuntime.cppJVM函数入口:InterpreterRuntime::monitorenter最终调用:src/share/vm/runtime/objectMonitor.cppObjectMonitor::entermonitorexit执行monitorexit指令位置:代码文件:src/share/vm/runtime/objectMonitor.cpp调用函数:ObjectMonitor::exit显示源码!如果本文对您有帮助,请关注并点赞;如果您有什么建议,也可以留言或私信。您的支持是我坚持创作的动力
