架构师总动员实现架构转型,再无中年危机
尼恩说在前面 在40岁老架构师 尼恩的读者交流群 (50+)中,很多小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试机会,遇到很多很重要的面试题: 1.请解释 JVM 偏向锁、轻量级锁、自旋锁、重量级锁什么? 2.请介绍一下什么是sychronized的自旋锁、偏向锁、轻量级锁、重量级锁? 4.请介绍一下 jvm 内置锁 的膨胀过程中锁内存怎么变化的,? 5.请介绍一下 jvm 内置锁 的 从轻量级锁升级重量级锁内存怎么变化的? 6.请介绍一下 jvm 锁的 膨胀过程?锁内存怎么变化的? 最近有小伙伴在面试阿里,又遇到了 jvm 内置锁 膨胀相关的面试题。小伙伴 支支吾吾的说了几句,没说清楚,面试挂了。 所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流” ,然后实现”offer直提”。 当然,上面的面试题以及参考答案,也会收入咱们的 《 尼恩Java面试宝典PDF 》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。 最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书 第二作者 尼恩 (40岁老架构师, 负责提升此文的 技术高度,让大家有一种 俯视 技术的感觉) 本文目录 - 尼恩说在前面
- 为啥内置锁存在多种状态?
- 为什么会存在锁升级现象?
- 一图总览:锁的内存变化及膨胀流程图
- 二、锁的四种状态
- 锁对比
- 2.4.1 偏向锁/轻量级锁/重量级锁
- 1. Java对象(Object实例)的三个部分 - 2. 对象结构中的核心字段作用 - 3. 对象结构中的字段长度 - 2.4.2 Mark Word的结构信息
- 不同锁状态下的Mark word字段结构 - 1. 无锁状态 - 2. 偏向锁状态 - 3. 轻量级锁状态 - 4. 重量级锁状态 - 三:如何获得偏向锁
- 四:如何膨胀到 轻量级锁
- 五:如何膨胀到 重量级锁
- 锁的内存结构变化 大总结
- 锁的膨胀流程
- 说在最后:有问题找老架构取经
为啥内置锁存在多种状态? 在JDK1.6版本之前,所有的Java内置锁都是重量级锁。重量级锁会造成CPU在用户态和核心态之间频繁切换,所以代价高、效率低。 JDK1.6版本为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”实现。 所以,在JDK1.6版本里内置锁一共有四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这些状态随着竞争情况逐渐升级。 内置锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种能升级却不能降级的策略,其目的是为了提高获得锁和释放锁的效率。 为什么会存在锁升级现象? 在 synchronized
最初的实现方式是 “阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间。 在java5及其以前,只有synchronized 这个是重量级锁,是操作系统级别的重量级操作。 如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长, java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程,就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等, 用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。 这也是在JDK6以前 synchronized
效率低下的原因,JDK6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。 一图总览:锁的内存变化及膨胀流程图 二、锁的四种状态 这种方式就是 synchronized
实现同步最初的方式,这也是当初开发者诟病的地方, 所以目前锁状态一种有四种,从级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级锁 ,锁状态只能升级,不能降级 锁状态 存储内容 标志位 对象的hashCode、对象分代年龄、是否是偏向锁(0) 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1)
如图所示: 在32位的虚拟机中: 在64位的虚拟机中: 锁对比 锁 优点 缺点 适用场景 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距
2.4.1 偏向锁/轻量级锁/重量级锁 总体而言,Java对象(Object实例)结构包括三部分:对象头、对象体、对齐字节。具体如图2-4所示。 1. Java对象(Object实例)的三个部分 对象头包括三个字段,第一个字段叫做_mark Word(标记字),用于存储自身运行时的数据例如GC标志位、哈希码、锁状态等信息。 第二个字段叫做 _klass
Pointer(类对象指针),用于存放此对象的元数据(InstanceKlass)的地址。通过 _klass
指针,虚拟机通过可以确定这个对象是哪个类的实例. 第二个字段叫做Array Length(数组长度)。如果对象是一个Java数组,那么此字段必须有,用于记录数组长度的数据;如果对象不是一个Java数组,那么此字段不存在,所以这是一个可选字段。 对象体包含了对象的实例变量(成员变量)。用于成员属性值,包括父类的成员属性值。这部分内存按4字节对齐。 对齐字节也叫做填充对齐,其作用是用来保证Java对象在所占内存字节数为8的倍数(8N bytes)。HotSpot VM的内存管理要求对象起始地址必须是8字节的整数倍。对象头本身是8的倍数,当对象的实例变量数据不是8的倍数,便需要填充数据来保证8字节的对齐。 2. 对象结构中的核心字段作用 接下来,对Object实例结构中几个重要的字段的作用做一下简要说明: (1)_mark(标记字)字段主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode。 (2)_klass(类对象指针)字段是一个指向方法区中类元数据信息的指针,意味着该对象可随时知道自己是哪个Class(实际为InstanceKlass)的实例。 (3)Array Length(数组长度)字段也占用32位(在32位JVM中)的字节,这是可选的,只有当本对象是一个数组对象时才会有这个部分。 (4)对象体用于保存对象属性值,是对象的主体部分,占用的内存空间大小取决于对象的属性数量和类型。 (5)对齐字节并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。当对象实例数据部分没有对齐(8字节的整数倍)时,就需要通过对齐填充来补全。 3. 对象结构中的字段长度 Mark Word(表_mark成员)、 _klass
Pointer(表_klass成员)、Array Length等字段的长度,都与JVM的位数有关。 Mark Word的长度为JVM的一个Word(字)大小,也就是说32位JVM的Mark Word为32位,64位JVM为64位。Klass Pointer(类对象指针)字段的长度也为JVM的一个Word(字)大小,即32位的JVM为32位,64位的JVM为64位。 在32位JVM虚拟机中,Mark Word和Klass Pointer这两部分都是32位的; 在64位JVM虚拟机中,Mark Word和Klass Pointer这两部分都是64位的。 对于对象指针而言,如果JVM中对象数量过多,使用64位的指针将浪费大量内存,通过简单统计,64位的JVM将会比32位的JVM多耗费50%的内存。 为了节约内存可以使用选项+UseCompressedOops开启指针压缩。 选项UseCompressedOops中的Oop部分为Ordinary object pointer普通对象指针的缩写。 如果开启UseCompressedOops选项,以下类型的指针将从64位压缩至32位: 2.4.2 Mark Word的结构信息 Java内置锁的涉及到很多重要信息,这些都存放在对象结构中,并且是存放于对象头的 Mark Word字段中。Mark Word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。 Mark Word的位长度不会受到Oop对象指针压缩选项的影响。 Java内置锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁。 其实在 JDK 1.6之前,Java内置锁还是一个重量级锁,是一个效率比较低下的锁,在JDK 1.6之后,JVM为了提高锁的获取与释放效率,对synchronized的实现进行了优化,引入了偏向锁、轻量级锁的实现,从此以后Java内置锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别)。 不同锁状态下的Mark word字段结构 Mark word字段的结构,与Java内置锁的状态强相关。为了让Mark word字段存储更多的信息,JVM将Mark word的最低两个位设置为Java内置锁状态位,不同锁状态下的32位Mark Word结构,如表2-1所示。 尼恩提示:以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。 1. 无锁状态 Java对象刚创建时,还没有任何线程来竞争,说明该对象处于无锁状态(无线程竞争它)这偏向锁标识位是0、锁状态01。 无锁状态下对象的Mark Word如图2-7所示。 2. 偏向锁状态 偏向锁是指一段同步代码一直被同一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。如果内置锁处于偏向状态,当有一个线程来竞争锁时,先用偏向锁,表示内置锁偏爱这个线程,这个线程要执行该锁关联的同步代码时,不需要再做任何检查和切换。偏向锁在竞争不激烈的情况下,效率非常高。 偏向锁状态的Mark Word会记录内置锁自己偏爱的线程ID,内置锁会将该线程当做自己的熟人。偏向锁状态下对象的Mark Word具体如图2-8所示。 3. 轻量级锁状态 当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象,锁对象的Mark Word就指向哪个线程的栈帧中的锁记录。轻量级锁状态下对象的Mark Word具体如图2-9所示。 当锁处于偏向锁的时候,而又被另一个线程所企图抢占时,偏向锁就会升级为轻量级锁。企图抢占的线程会通过自旋的形式尝试获取锁,不会阻塞抢锁线程,以便提高性能。 自旋原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。 但是线程自旋是需要消耗 CPU的,如果一直获取不到锁,那线程也不能一直占用 CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。 JVM 对于自旋周期的选择,JDK1.6之后引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不是固定的,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。 如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,自旋不会一直持续下去,这时争用线程会停止自旋进入阻塞状态,该锁膨胀为重量级锁。 4. 重量级锁状态 重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器(Monitor)对象,该监视器对象用集合的形式,来登记和管理排队的线程。重量级锁状态下对象的Mark Word具体如图2-10所示。 图2-10 重量级锁状态内置锁的Mark Word 三:如何获得偏向锁 synchronized (lock) { lock.increase(); if (i == MAX_TURN / 2) { Print.tcfo("占有锁, lock 的状态: "); lock.printObjectStruct(); } } 偏向锁的核心原理是:如果不存在 竞争的一个线程获得了锁,那么锁就从无锁状态,进入偏向状态,此时,Mark Word 的结构变为偏向锁结构, 然后线程的thread ID记录在锁对象的Mark Word中(使用CAS操作完成)。 以后该线程获取锁的时,判断一下线程ID和标志位,就可以直接进入同步块,连CAS操作都不需要,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。 偏向锁比较极致,干脆就把同步取消掉,不需要进行CAS了。 偏向锁的发现,主要得益于人们发现某个线程可以频繁的获取到锁。 如果某个锁资源一直是被某个线程获取,而且没有其它线程来获取锁,就可以在 Mark Word
中记录下这个线程id,该线程就没有必要花时间来进行CAS操作了,可以直接进入到同步代码块。 直到发现有其它线程来抢占锁资源了,就会根据当前状态判断是否把偏向锁膨胀成为轻量级锁。 如果需要使用偏向锁,可以使用参数: -XX:+UseBiased
参数来添加。 在JDK1.6之后是默认开启的,但是启动时间有延迟(4秒), 在JDK1.6之后,需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 关闭偏向锁:关闭之后程序默认会直接进入------->轻量级锁状态 尼恩提示:以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。 四:如何膨胀到 轻量级锁 多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞。 主要目的:在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋,不行才升级阻塞。 升级时机:当关闭偏向锁功能,或多线程竞争偏向锁会导致偏向锁升级为轻量级锁 假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。 而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。 如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A→B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程“被“释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位; 如果锁获取失败,则偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。 尼恩提示:以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。 轻量级锁也是在 JDK1.6 加入的,当一个线程获取偏向锁的时候,有另外的线程加入锁的竞争时,这个时候就会从偏向锁升级为轻量级锁。 在轻量级锁的状态时,虚拟机首先会在当前线程的栈帧当中建立一个锁记录(Lock Record),用于存储对象 MarkWord 的拷贝,官方称这个为 Displaced Mark Word。 然后虚拟机会使用 CAS 操作尝试将对象的 MarkWord 指向栈中的 Lock Record,如果操作成功说明这个线程获取到了锁,能够进入同步代码块执行,否则说明这个锁对象已经被其他线程占用了,线程就需要使用 CAS 不断的进行获取锁的操作,当然你可能会有疑问,难道就让线程一直死循环了吗? 这对 CPU 的花费那不是太大了吗,确实是这样的因此在 CAS 满足一定条件的时候轻量级锁就会升级为重量级锁,具体过程在重量级锁章节中分析。 当线程需要从同步代码块出来的时候,线程同样的需要使用 CAS 将 Displaced Mark Word 替换回对象的 MarkWord,如果替换成功,那么同步过程就完成了,如果替换失败就说明有其他线程尝试获取该锁,而且锁已经升级为重量级锁,此前竞争锁的线程已经被挂起,因此线程在释放锁的同时还需要将挂起的线程唤醒。 默认启用,默认情况下自旋的次数是10次,或者自旋线程数超过cpu核数一半。 变为自适应自旋锁。意味着自旋的次数不是固定不变的,而是根据:拥有锁线程的状态来决定,或者同一个锁上一次自旋的时间。 线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。 反之,如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。 在有两个以上的线程竞争同一个轻量级锁的情况下,轻量级锁不再有效(轻量级锁升级的一个条件),这个时候锁为膨胀成重量级锁,锁的标志状态变成 10,MarkWord 当中存储的就是指向重量级锁的指针,后面等待锁的线程就会被挂起。 因为这个时候 MarkWord 当中存储的已经是指向重量级锁的指针,因此在轻量级锁的情况下进入到同步代码块在出同步代码块的时候使用 CAS 将 Displaced Mark Word 替换回对象的 MarkWord 的时候就会替换失败,在前文已经提到,在失败的情况下,线程在释放锁的同时还需要将被挂起的线程唤醒。 五:如何膨胀到 重量级锁 Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。 当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。 在这种情况下需要操作系统将 没有抢到锁的线程挂起, JVM(Linux 操作系统下)底层是使用 pthread_mutex_lock 、 pthread_mutex_unlock 、 pthread_cond_wait 、 pthread_cond_signal 和 pthread_cond_broadcast 这几个库函数实现的,而这些函数依赖于 futex 系统调用,因此在使用重量级锁的时候因为进行了系统调用,进程需要从用户态转为内核态将线程挂起,然后从内核态转为用户态,当解锁的时候又需要从用户态转为内核态将线程唤醒,这一来二去的花费就比较大了(和 CAS 自旋锁相比)。 尼恩提示:以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。 锁的内存结构变化 大总结 锁状态 bits 1bit是否是偏向锁 2bit锁标志位
锁的膨胀流程 尼恩提示:以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。 说在最后:有问题找老架构取经 JVM锁的膨胀、锁的内存结构变化相关的面试题,是非常常见的面试题。也是核心面试题。也是非常难面试题。 以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。最终,让面试官爱到 “不能自已、口水直流” 。offer, 也就来了。 在面试之前,建议大家系统化的刷一波 5000页《 尼恩Java面试宝典 》V174,在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。 另外,如果没有面试机会,可以找尼恩来帮扶、领路。尼恩已经指导了大量的就业困难的小伙伴上岸. 前段时间,帮助一个40岁+就业困难小伙伴拿到了一个年薪100W 的offer,小伙伴实现了 逆天改命 。
如何 逆天改命,包含AI、大数据、golang、Java 等
实现职业转型,极速上岸
关注职业救助站 公众号,获取每天职业干货 助您实现职业转型、职业升级、极速上岸 ---------------------------------
实现架构转型,再无中年危机
关注技术自由圈 公众号,获取每天技术千货 一起成为牛逼的未来超级架构师
几十篇架构笔记、5000页面试宝典、20个技术圣经请加尼恩个人微信 免费拿走
暗号 ,请在 公众号后台 发送消息:领电子书
如有收获,请点击底部的"在看 "和"赞 ",谢谢