简介: 对象在内存中的内存布局是什么样的?如何描述synchronized和ReentrantLock的底层实现和重入的底层原理?为何AQS底层是CAS+volatile?锁的四种状态和锁升级过程应该如何描述?Object o = new Object() 在内存中占用多少字节?自旋锁是否是必定比重量级锁效率高?打开偏向锁是否效率必定会提高?重量级锁到底重在哪里?重量级锁何时比轻量级锁效率高,一样反之呢?带着这些问题往下读。java
做者 | 洋锅
来源 | 阿里技术公众号编程
总结AQS以后,对这方面顺带的复习一下。本文从如下几个高频问题出发:安全
无心识中用到锁的状况:服务器
//System.out.println都加了锁 public void println(String x) { synchronized (this) { print(x); newLine(); } }
简单加锁发生了什么?多线程
要弄清楚加锁以后到底发生了什么须要看一下对象建立以后再内存中的布局是个什么样的?并发
一个对象在new出来以后在内存中主要分为4个部分:jvm
知道了这4个部分以后,咱们来验证一下底层。借助于第三方包 JOL = Java Object Layout java内存布局去看看。很简单的几行代码就能够看到内存布局的样式:布局
public class JOLDemo { private static Object o; public static void main(String[] args) { o = new Object(); synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } } }
将结果打印出来:优化
从输出结果看:this
1)对象头包含了12个字节分为3行,其中前2行其实就是markword,第三行就是klass指针。值得注意的是在加锁先后输出从001变成了000。Markword用处:8字节(64bit)的头记录一些信息,锁就是修改了markword的内容8字节(64bit)的头记录一些信息,锁就是修改了markword的内容字节(64bit)的头记录一些信息。从001无锁状态,变成了00轻量级锁状态。
2)New出一个object对象,占用16个字节。对象头占用12字节,因为Object中没有额外的变量,因此instance = 0,考虑要对象内存大小要被8字节整除,那么padding=4,最后new Object() 内存大小为16字节。
拓展:什么样的对象会进入老年代?不少场景例如对象太大了能够直接进入,可是这里想探讨的是为何从Young GC的对象最多经历15次Young GC还存活就会进入Old区(年龄是能够调的,默认是15)。上图中hotspots的markword的图中,用了4个bit去表示分代年龄,那么能表示的最大范围就是0-15。因此这也就是为何设置新生代的年龄不能超过15,工做中能够经过-XX:MaxTenuringThreshold去调整,可是通常咱们不会动。
探讨锁的升级以前,先作个实验。两份代码,不一样之处在于一个中途让它睡了5秒,一个没睡。看看是否有区别。
public class JOLDemo { private static Object o; public static void main(String[] args) { o = new Object(); synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } } } ---------------------------------------------------------------------------------------------- public class JOLDemo { private static Object o; public static void main(String[] args) { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } o = new Object(); synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } } }
这两份代码会不会有什么区别?运行以后看看结果:
有点意思的是,让主线程睡了5s以后输出的内存布局跟没睡的输出结果竟然不同。
Syn锁升级以后,jdk1.8版本的一个底层默认设置4s以后偏向锁开启。也就是说在4s内是没有开启偏向锁的,加了锁就直接升级为轻量级锁了。
那么这里就有几个问题了?
问题1:为何要进行锁升级?锁了就锁了,不就要加锁么?
首先明确早起jdk1.2效率很是低。那时候syn就是重量级锁,申请锁必需要通过操做系统老大kernel进行系统调用,入队进行排序操做,操做完以后再返回给用户态。
内核态:用户态若是要作一些比较危险的操做直接访问硬件,很容易把硬件搞死(格式化,访问网卡,访问内存干掉、)操做系统为了系统安全分红两层,用户态和内核态 。申请锁资源的时候用户态要向操做系统老大内核态申请。Jdk1.2的时候用户须要跟内核态申请锁,而后内核态还会给用户态。这个过程是很是消耗时间的,致使早期效率特别低。有些jvm就能够处理的为何还交给操做系统作去呢?能不能把jvm就能够完成的锁操做拉取出来提高效率,因此也就有了锁优化。
问题2:为何要有偏向锁?
其实这本质上归根于一个几率问题,统计表示,在咱们平常用的syn锁过程当中70%-80%的状况下,通常都只有一个线程去拿锁,例如咱们常使用的System.out.println、StringBuffer,虽然底层加了syn锁,可是基本没有多线程竞争的状况。那么这种状况下,没有必要升级到轻量级锁级别了。偏向的意义在于:第一个线程拿到锁,将本身的线程信息标记在锁上,下次进来就不须要在拿去拿锁验证了。若是超过1个线程去抢锁,那么偏向锁就会撤销,升级为轻量级锁,其实我认为严格意义上来说偏向锁并不算一把真正的锁,由于只有一个线程去访问共享资源的时候才会有偏向锁这个状况。
无心使用到锁的场景:
/***StringBuffer内部同步***/ public synchronized int length() { return count; } //System.out.println 无心识的使用锁 public void println(String x) { synchronized (this) { print(x); newLine(); } }
问题3:为何jdk8要在4s后开启偏向锁?
其实这是一个妥协,明确知道在刚开始执行代码时,必定有好多线程来抢锁,若是开了偏向锁效率反而下降,因此上面程序在睡了5s以后偏向锁才开放。为何加偏向锁效率会下降,由于中途多了几个额外的过程,上了偏向锁以后多个线程争抢共享资源的时候要进行锁升级到轻量级锁,这个过程还的把偏向锁进行撤销在进行升级,因此致使效率会下降。为何是4s?这是一个统计的时间值。
固然咱们是能够禁止偏向锁的,经过配置参数-XX:-UseBiasedLocking = false来禁用偏向锁。jdk15以后默认已经禁用了偏向锁。本文是在jdk8的环境下作的锁升级验证。
上面已经验证了对象从建立出来以后进内存从无锁状态->偏向锁(若是开启了)->轻量级锁的过程。对于锁升级的流程继续往下,轻量级锁以后就会变成重量级锁。首先咱们先理解什么叫作轻量级锁,从一个线程抢占资源(偏向锁)到多线程抢占资源升级为轻量级锁,线程若是没那么多的话,其实这里就能够理解为CAS,也就是咱们说的Compare and Swap,比较并交换值。在并发编程中最简单的一个例子就是并发包下面的原子操做类AtomicInteger。在进行相似++操做的时候,底层其实就是CAS锁。
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
问题4:什么状况下轻量级锁要升级为重量级锁呢?
首先咱们能够思考的是多个线程的时候先开启轻量级锁,若是它carry不了的状况下才会升级为重量级。那么什么状况下轻量级锁会carry不住。一、若是线程数太多,好比上来就是10000个,那么这里CAS要转多久才可能交换值,同时CPU光在这10000个活着的线程中来回切换中就耗费了巨大的资源,这种状况下天然就升级为重量级锁,直接叫给操做系统入队管理,那么就算10000个线程那也是处理休眠的状况等待排队唤醒。二、CAS若是自旋10次依然没有获取到锁,那么也会升级为重量级。
总的来讲2种状况会从轻量级升级为重量级,10次自旋或等待cpu调度的线程数超过cpu核数的一半,自动升级为重量级锁。看服务器CPU的核数怎么看,输入top指令,而后按1就能够看到。
问题5:都说syn为重量级锁,那么到底重在哪里?
JVM偷懒把任何跟线程有关的操做所有交给操做系统去作,例如调度锁的同步直接交给操做系统去执行,而在操做系统中要执行先要入队,另外操做系统启动一个线程时须要消耗不少资源,消耗资源比较重,重就重在这里。
整个锁升级过程如图所示:
上面咱们对对象的内存布局有了一些了解以后,知道锁的状态主要存放在markword里面。这里咱们看看底层实现。
public class RnEnterLockDemo { public void method() { synchronized (this) { System.out.println("start"); } } }
对这段简单代码进行反解析看看什么状况。javap -c RnEnterLockDemo.class
首先咱们能肯定的是syn确定是还有加锁的操做,看到的信息中出现了monitorenter和monitorexit,主观上就能够猜到这是跟加锁和解锁相关的指令。有意思的是1个monitorenter和2个monitorexit。为何呢?正常来讲应该就是一个加锁和一个释放锁啊。其实这里也体现了syn和lock的区别。syn是JVM层面的锁,若是异常了不用本身释放,jvm会自动帮助释放,这一步就取决于多出来的那个monitorexit。而lock异常须要咱们手动补获并释放的。
关于这两条指令的做用,咱们直接参考JVM规范中描述:
monitorenter :
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor. • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count. • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership
翻译一下:
每一个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的全部权,过程以下:
monitorexit:
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
翻译一下:
执行monitorexit的线程必须是objectref所对应的monitor的全部者。指令执行时,monitor的进入数减1,若是减1后进入数为0,那线程退出monitor,再也不是这个monitor的全部者。其余被这个monitor阻塞的线程能够尝试去获取这个 monitor的全部权。
经过这段话的描述,很清楚的看出Synchronized的实现原理,Synchronized底层经过一个monitor的对象来完成,wait/notify等方法其实也依赖于monitor对象,这就是为何只有在同步的块或者方法中才能调用wait/notify等方法,不然会抛出java.lang.IllegalMonitorStateException的异常。
每一个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,若是目标对象的计数器为零,那么说明它没有被其余线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,而且将其计数器加i。在目标锁对象的计数器不为零的状况下,若是锁对象的持有线程是当前线程,那么Java虚拟机能够将其计数器加1,不然须要等待,直至持有线程释放该锁。当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零表明锁已被释放。
以往的经验中,只要用到synchronized就觉得它已经成为了重量级锁。在jdk1.2以前确实如此,后来发现过重了,消耗了太多操做系统资源,因此对synchronized进行了优化。之后能够直接用,至于锁的力度如何,JVM底层已经作好了咱们直接用就行。
最后再看看开头的几个问题,是否是都理解了呢。带着问题去研究,每每会更加清晰。但愿对你们有所帮助。
原文连接本文为阿里云原创内容,未经容许不得转载。