说到锁,就会提到synchronized。这个英文单词是什么意思?翻译成中文就是“同步”的意思。一般用关键字synchronized来锁定一段代码或一个方法,使这段代码或方法在同一时刻只能有一个。线程来执行它。与volatile相比,synchronized更加灵活。您可以在方法、静态方法或代码块上使用它。这就是同步部分的内容。今天阿芬想重点说说synchronized底层是怎么实现的。JVM是如何实现synchronized的?我知道synchronized关键字可以用来给程序加锁,但是具体是怎么实现的我也不知道,别着急,先看一个demo:publicclassdemo{publicvoidsynchronizedDemo(Objectlock){synchronized(lock){lock.hashCode();}}}上面是我写的一个demo,然后进入class文件所在目录,使用javap-vdemo.class查看编译后的字节码(我截取了一部分这里):publicvoidsynchronizedDemo(java.lang.Object);描述符:(Ljava/lang/Object;)Vflags:ACC_PUBLICode:stack=2,locals=4,args_size=20:aload_11:dup2:astore_23:monitorenter4:aload_15:invokevirtual#2//Methodjava/lang/Object.hashCode:()I8:pop9:aload_210:monitorexit11:goto1914:astore_315:aload_216:monitorexit17:aload_318:athrow19:returnExceptiontable:fromtotargettype41114any141714any你应该能够看到当程序声明一个同步代码块,编译后的字节码会包含monitorenter和monitorexit指令,这些two指令会消耗操作数栈上一个引用类型的元素(即synchronized关键字括号中的引用)作为锁对象进行加锁和解锁。仔细看,上面有一条monitorenter指令和两条monitorexit指令,是Java虚拟机用来保证获取到的锁无论是在正常执行路径上还是异常执行路径上都能解锁的指令。关于monitorenter和monitorexit,可以理解为每个锁对象都有一个锁计数器和一个指向持有锁的线程的指针:当程序执行monitorenter时,如果目标锁对象的计数器为0,则表示没有正在被其他线程使用这时,如果有线程请求使用,Java虚拟机就会分配给该线程,并将计数器值加1。当目标锁对象的计数器不为0时,如果锁对象持有的线程是当前线程,Java虚拟机可以将其计数器加1。如果不?抱歉,我们只能等待释放线程。Java虚拟机在执行monitorexit的时候,会将锁对象的计数器减1,这时候就意味着锁被释放了。如果此时有其他线程请求,则可以请求成功。为什么使用这种方法?就是让同一个线程可以重复获取同一个锁。比如一个Java类中有很多synchronized方法,这些方法之间的相互调用,无论是直接调用还是间接调用,都会涉及到对同一个锁的重复加锁操作。如果这样设计,就可以避免这种情况。锁在Java多线程中,所有锁都是基于对象的。换句话说,Java中的每个对象都可以用作锁。你可能会有疑问,不对,有类锁。但是类对象也是特殊的Java对象,所以Java中所有的锁都是基于对象的。在Java6之前,所有的锁都是“重量级”锁。重量级锁会造成一个问题,就是如果程序频繁的获取和释放锁,会导致性能的极大消耗。为了优化这个问题,引入了“偏向锁”和“轻量级锁”的概念。所以在Java6及之后的版本中,一个对象有4种锁状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。四种锁状态中,无锁状态应该比较容易理解。没有锁就是没有锁,任何线程都可以尝试修改,这里就提一下。随着竞争的出现,锁的升级很容易发生,但是如果要降级锁,条件就非常苛刻了。有一种想来就来,想走就去不了的匆忙。这里啰嗦一句:很多文章都说锁升级了就不能降级了。其实在HotSpotJVM中,是支持锁降级的。锁降级发生在StopTheWorld期间。当JVM进入安全点后,会检查是否有空闲锁?如果有,你会尝试降级。当您看到StopTheWorld和安全点时,有些人可能会感到困惑。我在这里简单说一下。需要读者自行探索。本文的重点不是JVM)在Java虚拟机中,传统的垃圾回收算法采用了一种简单粗暴的方法,就是Stop-the-world,而这个Stop-the-world就是通过安全点(safepoint)安全是什么意思?也就是说,当Java程序执行native代码时,如果代码没有访问Java对象/调用Java方法/返回原来的Java方法,那么Java虚拟机的栈就不会发生变化,也就是说执行的本机代码可以用作安全点。当Java虚拟机收到Stop-the-world请求时,它会等待所有线程到达安全点,然后才允许请求Stop-the-world的线程执行独占工作。接下来,我们将介绍几种锁和锁的升级。Java对象头一开始就说Java锁都是基于对象的,那我怎么告诉程序我是锁呢?不得不说,每个Java对象都有一个对象头。如果是非数组类型,用2个字符存放对象头,如果是数组,用3个字符存放对象头。在32位处理器中,字宽为32位;在64位处理器中字宽为64位~对象头内容如下:Length内容描述32/64位MarkWord存储对象hashCode或Lock信息等32/64位ClassMetadataAddressPointer存储到对象类型数据32/64位Arraylength数组的长度(如果是数组的话)主要看MarkWord的内容:锁状态29位/61位1位是否是偏向锁2位Lockflag无锁001偏向锁线程ID101轻量级锁指向栈中锁记录的指针此时不使用该位标识偏向锁00重量级锁指向mutex(重量级锁)该位暂不用于标识偏向锁10GCmark该位暂不用于标识偏向锁11从上表应该可以看出,当是偏向锁时,MarkWord中存储的是偏向锁的线程ID;当是轻量级锁时,MarkWord在线程栈中存储一个指向LockRecord的指针;它是一个重量级的加锁时,MarkWord将指向监控对象的指针存储在堆中。偏向锁HotSpot的作者经过大量研究发现,在大多数情况下,不仅不存在多线程对锁的竞争,反而总是被同一个线程多次获取。基于此,引入了偏向锁的概念,那么什么是偏向锁呢?说白了就是我给锁设置一个变量,当有线程请求的时候,发现锁为真,也就是说这个时候没有锁。资源竞争,那么就不需要经过加锁/解锁的过程,直接使用即可。但是如果lock为false,说明还有其他线程在竞争资源,那么我们通过正式的流程来看看具体的实现原理:当线程第一次进入synchronized块时,偏向锁的线程ID保存在对象头和栈帧中的锁记录中。当线程下次进入同步块时,会检查锁定的MarkWord是否存储了自己的线程ID。如果是,则说明该线程已经获得了锁,那么该线程在进入和退出同步块时,不需要花费CAS操作进行加锁和解锁;如果没有,说明有另外一个线程在竞争这个偏向锁,然后会尝试使用CAS将MarkWord中的线程ID替换成新的线程ID。这时候会出现两种情况:替换成功,说明之前的线程不存在,那么MarkWord中的线程ID就是新线程的ID,不会升级锁。此时偏向锁的替换仍然失败,说明之前的线程如果还存在,则挂起之前的线程,设置偏向锁标志为0,同时设置锁标志为00,升级为轻量级锁,按照轻量级锁法取消竞争锁。在释放锁之前等待直到争用发生的机制。也就是说,如果没有人来和我争这把锁,那么这把锁就是我独有的。当其他线程试图与我竞争偏向锁时,我会在偏向锁升级为轻量级锁时释放锁,拥有偏向锁的线程会先被挂起,偏向锁标志位会被重置。这个过程看似很简单,但是开销很高,因为:首先需要把拥有锁的线程停在安全点,然后遍历线程栈。如果有锁记录,则需要修复锁记录和MarkWord,变成无锁状态,最后唤醒停止的线程,将偏向锁升级为轻量级锁。你以为是升级一把轻量化锁?tooyoungtoosimplebiased锁升级为轻量级锁的过程非常耗费资源。如果应用程序中的所有锁通常都处于竞争状态,此时偏向锁就是一种负担。这时候可以通过JVM参数禁用偏向锁:-XX:-UseBiasedLocking=false,那么程序会默认进入轻量级锁状态。JVM会使用轻量级锁来避免线程阻塞和唤醒轻量级锁。加锁JVM会为每个线程在当前线程的栈帧中创建一个空间用于存放锁记录,称为DisplacedMarkWord。如果线程在获取锁的时候发现自己是轻量级锁,就会将锁的MarkWord复制到自己的DisplacedMarkWord中。然后该线程会尝试使用CAS将锁MarkWord替换为指向锁记录的指针。如果替换成功,当前线程获取到锁,那么整个状态还是轻量级锁状态。如果更换失败?说明MarkWord已经被其他线程的锁记录替换了,那就尝试使用自旋来获取锁。(自旋即线程不断尝试获取锁,通常通过循环实现)自旋消耗CPU。如果获取不到锁,线程就会一直自旋,CPU宝贵的资源就白白浪费了。解决这个问题最简单的方法是指定旋转次数。比如替换不成功,就循环10次。如果还没有拿到,就会进入阻塞状态。但是JDK使用了一个更聪明的方法---Adaptivespin。也就是说,如果这次线程自旋成功了,那我下次再自旋更多次,因为我这次自旋成功了,说明我成功的概率还是挺大的,下次自旋的次数会more,那么如果旋转失败,我会减少下一次旋转的次数。比如我看到了失败的迹象,那么我就先滑倒,而不是“不撞南墙不回头”。自旋失败后,线程会被阻塞,锁升级为重量级锁。轻量级锁释放:释放锁时,当前线程会使用CAS操作将DisplacedMarkWord中的内容复制到加锁的MarkWord中。如果没有竞争,则复制操作成功;如果其他线程由于多次自旋导致轻量级锁升级为重量级锁,CAS操作就会失败,此时锁会被释放,被阻塞的会被唤醒。过程是一样的,这里放一张图:重量级锁重量级锁是靠操作系统的互斥锁(mutex)来实现的。但是操作系统中线程之间的转换需要比较长的时间(因为操作系统需要从用户态切换到内核态,代价很大),所以重量级锁效率很低,但有一点是它们是blocked每一个不消耗CPU的对象都可以看作是一个锁,那么当多个线程同时请求一个对象锁时,它会如何处理呢?对象锁会设置一个集中状态来区分请求线程:ContentionList:所有请求锁的线程将被放在竞争队列的??第一位EntryList:那些在ContentionList中有资格成为候选者的线程被移动到EntryListWaitSet:调用wait方法阻塞的线程会被放入WaitSet中OnDeck:任何时候最多有一个线程在竞争锁,该线程称为OnDeckOwner:获得锁的线程称为Owner!Owner:释放锁的线程当一个线程试图获取锁时,如果锁被占用,则将线程封装成一个ObjectWaiter对象插入到ContentionList队列的头部,然后是park函数将被调用以挂起当前线程。当线程释放锁时,会从ContentionList或EntryList中选出一个线程唤醒。如果线程在获取锁后调用了Object.wait方法,线程会被放入WaitSet,当被Object.notify唤醒后,线程会从WaitSet移动到ContentionList或EntryList.但是,当调用锁对象的wait或notify方法时,如果锁的当前状态是偏向锁或轻量级锁,会先展开为重量级锁。保证锁当一个线程即将获取共享资源时:首先检查MarkWord中的ThreadID是否为自己的ThreadID,如果是,则说明当前线程处于“偏向锁”,如果不是,则升级锁,并此时使用CAS操作来进行切换。新线程根据MarkWord中已有的ThreadID通知前一个线程挂起,并将MarkWord的内容设置为空。然后,两个线程将锁对象的HashCode复制到自己新建的用于存放锁的记录空间,然后开始通过CAS操作,将锁对象的MarkWord的内容修改为新建的记录空间的地址,竞争MarkWord这样,执行CAS成功的线程获取资源,失败的线程在自旋过程中会进入自旋-自旋过程,如果成功获取资源(也就是之前获取资源的线程被执行,共享资源被释放),那么整个状态还是处于轻量级锁的状态。如果没有获取到资源,就会进入重量级锁状态。阻塞自旋线程,等待前一个线程执行完成并唤醒自己。没想到这篇文章是阿粉写的,5000多字(这篇文章阿粉不告诉你,是阿粉放假早上八点开始写,一直写到五六点下午,累死我了,看这里的大家,都是最美的自在
