1.为什么要说这个?总结完AQS,顺便回顾一下这方面。本文从以下高频问题入手:对象在内存中的内存布局是怎样的?描述synchronized和ReentrantLock的底层实现以及重入的底层原理。让我们谈谈AQS。为什么AQSCAS+底层会波动?描述一下锁的四种状态以及锁的升级过程?Objecto=newObject()在内存中占用多少字节?自旋锁一定比重量级锁更高效吗?高的?是否会提高开启偏向锁的效率?重量级锁的重量是多少?什么时候重量级锁比轻量级锁更高效,反之亦然?第二把锁怎么了?无意识的使用锁://System.out.println被锁publicvoidprintln(Stringx){synchronized(this){print(x);newLine();}}简单锁怎么了?想知道加锁后发生了什么需要查看对象创建后在内存中的布局是怎样的?一个对象在new之后在内存中主要分为4个部分:标记部分其实是加锁的核心,也包含了对象的一些生命信息,比如是否GC,经过几次YoungGC,它还活着。klass指针记录了指向对象的类文件指针。实例数据记录对象内部的变量数据。填充用于对齐。在对象的64位服务器版本中,规定了对象内存必须能被8字节整除。如果不可整除,则通过对齐方式填充。比如:new创建一个对象,内存只占18个字节,但是规定可以被8整除,所以padding=6。知道了这4个部分,我们来验证一下底层。借助第三方包JOL=JavaObjectLayoutjava内存布局来看。你可以通过几行简单的代码看到内存布局样式:.toPrintable());}}}打印结果:从输出结果来看:1)对象头包含12个字节,分为3行,前2行实际上是markword,第三行是klass指针。值得注意的是,锁定前后输出从001变为000。Markword的用途:8字节(64bit)的header记录了一些信息,锁是修改markword的内容。8字节(64bit)的header记录了一些信息,lock就是修改markword的内容。64bit的header记录了一些信息信息。从001无锁状态到00轻量级锁状态。2)new创建一个object对象,占用16个字节。对象头占用12个字节。由于Object中没有额外的变量,instance=0。考虑到object的内存大小必须能被8字节整除,那么padding=4。最后,newObject()的内存大小为16字节。扩展:什么样的对象会进入老区?很多场景下,比如对象太大,可以直接进入,但是这里要讨论的是,为什么YoungGC出来的对象最多存活15次就会进入Old区(年龄是可以调整的,默认是15岁)。上图的热点标记中,用了4位来表示代际年龄,所以可以表示的最大范围是0-15。所以这就是为什么新生代的年龄不能设置超过15的原因。可以在工作中通过-XX:MaxTenuringThreshold来调整,但是一般我们不会动。三锁升级过程1锁升级验证在讨论锁升级之前,我们先做一个实验。两段代码,不同的是中间一段让它休眠了5秒,一段没有休眠。看看有没有区别。publicclassJOLDemo{privatestaticObjecto;publicstaticvoidmain(String[]args){o=newObject();synchronized(o){System.out.println(ClassLayout.parseInstance(o).toPrintable());}}}-----------------------------------------------------------------------------------------publicclassJOLDemo{privatestaticObjecto;publicstaticvoidmain(String[]args){try{Thread.sleep(5000);}catch(InterruptedExceptione){e.printStackTrace();}o=newObject();synchronized(o){System.out.println(ClassLayout.parseInstance(o).toPrintable());}}}这两个代码之间会有什么区别吗?运行后看结果:有意思的是,让主线程休眠5s后的内存布局输出其实和没有休眠的输出是不一样的。Syn锁升级后,4s后会开启jdk1.8版本的底层默认设置。也就是说偏向锁在4秒内没有打开,锁直接升级为轻量级锁。那么这里有几个问题?为什么需要升级锁?之前默认的syn不是重量级锁吗?要么不使用它,要么使用其他东西?既然在4s之内加一把锁,就直接上轻量级,那我们能不能不要偏向锁,为什么要偏向锁呢?为什么我们需要在4s之后设置偏向锁呢?问题一:为什么要升级锁具?一上锁就上锁,为什么不加锁呢?首先澄清一下jdk1.2早期的效率很低。当时syn是重量级的锁。要申请锁,操作系统老大内核必须进行系统调用,进入队列进行排序操作,操作完成后返回用户态。内核态:如果用户态做了一些危险的操作,直接访问硬件,很容易把硬件干掉(格式化、访问网卡、访问内存等)。操作系统为了系统安全分为两层,用户态和内核态。申请锁资源时,用户态要向操作系统老大的内核态申请。在Jdk1.2中,用户需要向内核态申请锁,然后内核态也会给用户态。这个过程非常耗时,导致早期效率特别低。为什么要让操作系统来处理一些jvm可以处理的事情呢?能不能把jvm能完成的锁操作拉出来提高效率,所以有锁优化。问题2:为什么会出现偏向锁?其实这本质上是一个概率问题。统计显示,在我们日常使用的70%-80%的syn锁中,一般只有一个线程获取锁,例如我们经常使用的System.out.println和StringBuffer,虽然底层添加了syn锁,基本没有多线程竞争。在这种情况下,没有必要升级到轻量级锁级别。bias的意思是:第一个线程拿到锁,并在锁上标记自己的线程信息。下次进来就不用拿锁验证了。如果有多个线程抢到锁,则取消偏向锁,升级为轻量级锁。其实我认为偏向锁并不是严格意义上真正的锁,因为只有一个线程访问共享资源。这样才会有偏向锁。无意使用锁的场景:/***StringBufferInternalSynchronization***/publicsynchronizedintlength(){returncount;}//System.out.println无意使用锁publicvoidprintln(Stringx){synchronized(this){print(x);newLine();}}问题3:为什么jdk8会在4s后开启偏向锁?其实这是一种妥协。很明显,代码第一次执行的时候肯定有很多线程去抢锁。如果打开了偏向锁,效率就会倒过来。较低,所以上面的程序只会在休眠5s后打开。为什么加偏向锁效率会降低,是因为中间多了几个进程,当多个线程使用偏向锁后争抢共享资源时,必须将锁升级为轻量级锁。此过程还执行偏向锁。撤消正在升级,因此效率会降低。为什么是4s?这是一个统计时间值。当然,我们可以通过配置参数-XX:-UseBiasedLocking=false来禁用偏向锁来禁用偏向锁。jdk15.0之后默认禁用了偏向锁。本文是在jdk8环境下做的锁升级验证。2、锁的升级过程上面已经验证过了。对象创建后,从无锁状态进入内存->偏向锁(如果启用)->轻量级锁流程。锁升级的过程继续往下,轻量级的锁后面会变成重量级的锁。首先我们来了解一下什么是轻量级锁。从单线程抢占资源(偏向锁)到多线程抢占资源升级为轻量级锁,如果没有那么多线程,这个其实可以理解为CAS,也就是我们说的CompareandSwap,比较交换值.并发编程中最简单的例子就是concurrent包下的原子操作类AtomicInteger。在执行类似于++的操作时,底层其实是一个CAS锁。publicclassJOLDemo{privatestaticObjecto;publicstaticvoidmain(String[]args){o=newObject();synchronized(o){System.out.println(ClassLayout.parseInstance(o).toPrintable());}}}-----------------------------------------------------------------------------------------publicclassJOLDemo{privatestaticObjecto;publicstaticvoidmain(String[]args){try{Thread.sleep(5000);}catch(InterruptedExceptione){e.printStackTrace();}o=newObject();synchronized(o){System.out.println(ClassLayout.parseInstance(o).toPrintable());}}}问题四:轻量级锁在什么情况下升级为重量级锁?首先我们可以考虑在多线程的时候先开启轻量级锁,对于重量级的只有扛不住才升级。那么轻量级锁在什么情况下不会携带。1.如果线程太多,比如有10000个线程,CAS交换值需要多长时间,CPU光是在10000个活着的线程中来回切换,就消耗了巨大的资源。这种情况下,自然升级为重量级锁,直接调用操作系统入队管理。那么即使有10000个线程,它们仍然在处理休眠的情况,等待被排队唤醒。2、如果CAS自旋10次仍未获取到锁,则升级为重量级。总的来说,这两款机箱都会从轻量级升级到重量级。如果自旋10次或者等待CPU调度的线程数超过CPU核数的一半,锁会自动升级为重量级。要查看服务器CPU的核心数,输入top命令,然后按1即可查看。问题5:据说syn是重量级锁,有什么意义呢?JVM懒得把所有与线程相关的操作都交给操作系统。比如调度锁的同步是直接交给操作系统执行的,而在操作中要在系统中执行,则必须先入队。另外,操作系统启动一个线程时,需要消耗大量的资源。资源消耗比较重,最重要的是这里。整个锁的升级过程如图所示:4.synchronized的底层实现我们对上面对象的内存布局有了一定的了解后,我们知道锁的状态主要存储在mark中。下面我们看看底层的实现。publicclassRnEnterLockDemo{publicvoidmethod(){synchronized(this){System.out.println("start");}}}反分析这段简单的代码,看看是怎么回事。javap-cRnEnterLockDemo.class首先我们可以确定syn中肯定有加锁操作,我们看到的信息中出现了monitorenter和monitorexit。主观上我们可以猜测这是一条与加锁和解锁相关的指令。有趣的是1个monitorenter和2个monitorexit。为什么?正常来说,应该是加锁和释放锁。其实syn和lock的区别也体现在这里。Syn是JVM级别的锁。如果出现异常,不需要自己释放,jvm会自动帮助释放。这一步依赖于额外的monitorexit。锁异常需要我们手动捕获并释放。关于这两条指令的作用,我们直接参考JVM规范中的描述:monitorenter:每个对象关联一个monitor。当且仅当监视器有所有者时,它才会被锁定。执行monitorenter的线程试图获得与objectref关联的监视器的所有权,如下所示:如果与objectref关联的监视器的条目计数为零,则线程进入监视器并将其条目计数设置为一。线程就是监视器的所有者。?如果线程已经拥有与objectref关联的监视器,它会重新进入监视器,并增加其条目计数。?如果另一个线程已经拥有与objectref相关联的监视器,则该线程会阻塞,直到监视器的条目计数为零,然后再次尝试获得拥有一个对象的监视器锁(monitor)。当显示器被占用时,它将处于锁定状态。当线程执行monitorenter指令时,它会尝试获取monitor的所有权。过程如下:如果monitorentrynumber为0,则线程进入monitor,然后设置entrynumber为1,线程就是monitor的owner。如果线程已经占用管程,刚重新进入,则进入管程的次数加1。如果其他线程已经占用了管程,则该线程进入阻塞状态,直到管程数为0,然后再次尝试获取管程的所有权。monitorexit:执行monitorexit的线程必须是与objectref引用的实例关联的monitor的所有者。线程递减与objectref关联的监视器的条目计数。如果结果是条目计数的值为零,则线程退出监视器并且不再是它的所有者。其他阻塞进入监视器的线程被允许尝试这样做。翻译:执行monitorexit的线程必须是objectref对应的monitor的owner。指令执行时,monitor的entrynumber减1,如果entrynumber减1后为0,线程退出monitor,不再是monitor的owner。被该监视器阻塞的其他线程可以尝试取得该监视器的所有权。通过这一段的描述,我们可以清楚的看到Synchronized的实现原理。Synchronized底层是通过一个监听对象来完成的。其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在synchronized块或方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException异常。每个锁对象都有一个锁计数器和一个指向持有锁的线程的指针。在执行monitorenter时,如果目标对象的计数器为零,则说明它没有被其他线程持有,Java虚拟机将锁对象的持有线程设置为当前线程,并将其计数器加i。在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,Java虚拟机可以将计数器加1,否则需要等到持有线程释放锁。Java虚拟机在执行monitorexit时,需要将锁对象的计数器减1,计数器为0表示锁已经释放。总结以往的经验,只要用了synchronized,就认为已经成为重量级锁了。jdk1.2之前是这样,但是发现太重了,占用操作系统资源太多,所以优化了synchronized。以后可以直接使用。至于锁的强度,JVM底层已经准备好了,我们可以直接使用。最后看看前几题,你都看懂了吗?带着问题去研究,往往会变得更加清晰。我希望能有所帮助。
