前言
10年积累的网站设计制作、成都网站设计经验,可以快速应对客户对网站的新想法和需求。提供各种问题对应的解决方案。让选择我们的客户得到更好、更有力的网络服务。我虽然不认识你,你也不认识我。但先做网站后付款的网站建设流程,更有安宁免费网站建设让你可以放心的选择与我们合作。
大家好,我是狂聊。
今天来聊synchronized关键字,高频面试问题。
这篇文章构思 + 画图 + 文字花了好几天的时间,我已经彻底废了,看完希望你能有所收获。
话不多说,直接干货。
正文
一、synchronized的用法
1.1、三种使用方式
代码示例:
- public class Test {
- //对象
- Object object=new Object();
- //共享变量
- private static int num;
- //静态方法
- public synchronized static void lock1(){
- num ++;
- }
- //普通方法
- public synchronized void lock2(){
- num ++;
- }
- public void lock3(){
- //代码块
- synchronized (object){
- num ++;
- }
- }
- }
1.2、作用范围
面试时经常会问:synchronized 关键字锁的是什么?或者说它的作用范围是什么?
总结一下:
1.3、原子性、可见性、有序性
我们都知道并发编程需要考虑三个问题:原子性、可见性、有序性。
那么,使用 synchronized 关键字是如何解决这三个问题的?
二、对象内存布局
上面说了,这三种方式都是锁的是对象、对象、对象(说三遍),但是听起来好像很抽象的样子,对象还能被锁?该如何操作?
其实是和对象内存布局有关系。
耳听为虚,眼见为实,下面让你亲眼看到对象是由啥组成的。
示例代码:
- //1、需要导入包
- import org.openjdk.jol.info.ClassLayout;
- //2、定义Lock类
- public class Lock {
- int i;
- boolean flag;
- }
- //3、将Lock对象打印出来
- public class Test {
- public static void main(String[] args){
- Lock lock = new Lock();
- System.out.println(ClassLayout.parseInstance(lock).toPrintable());
- }
- }
打印出来的结果是这样的:
- OFFSET SIZE TYPE DESCRIPTION VALUE
- 0 4 (object header) 01 47 70 9d (00000001 01000111 01110000 10011101) (-1653586175)
- 4 4 (object header) 11 00 00 00 (00010001 00000000 00000000 00000000) (17)
- 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
- 12 4 int L.i 0
- 16 1 boolean L.flag false
- 17 7 (loss due to the next object alignment)
- Instance size: 24 bytes
- Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
对打印结果,详细解释一下:
2.1、对象头(Object Header)
Object Header 是 MarkWord 和 Class Pointer 组成的,后面会详细解释。
打印结果:占用 4+4+4=12 个 bytes。
2.2、实例数据(Interface Data)
对象实例数据包括了对象的所有成员变量,其大小由各个成员变量大小决定的。
当然,不包括静态成员变量,因为它是在方法区维护的!
打印结果:可以看到 int L.i 和 boolean L.flag 就是实例数据,占用 4+1=5 个 bytes。
2.3、填充数据(Padding)
Java 对象占用空间是 8 字节对齐的,即所有 Java 对象占用 bytes 数必须是 8 的倍数,因为当我们从磁盘中取一个数据时,不会是一个字节的去读,都是按照一整块来读取的,这一块大小就是 8 个字节,所以为了完整,padding 的作用就是补充字节,保证对象是 8 字节的整数倍。
打印结果:可以看到(loss due to the next object alignment) 这个就是填充数据,占用 7个字节。
这样的话,12+5+7=24 一共是 24 个 bytes,正好是 8 的倍数。
所以说,一个对象的内存布局是由对象头、实例数据、填充数据组成的。
接下来:重点关注这个对象头。
三、细说对象头
上面提到了对象头,直接看官网上的解释,官网地址在文末:
3.1、对象头(object header)
3.2、Klass Point
3.3、Mark Word
总结一下:其实对象头就是 MarkWord 和 Klass Point 组成的。MarkWord 是用来存储对象的 hashCode、锁信息或分代年龄或 GC 标志等信息。Klass Point 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
那么问题来了!!
问题:那上面说的 MarkWord 是存储的 hashcode、锁信息或分代年龄或 GC 标志是在那定义的呢?
你可以下载 OpenJDK 的源码,在 markOop.hpp 的文件中可以看到 Mark Word 的状态信息:
markOop.hpp
可以看到还是写的非常清晰的,画图总结一下:
Mark Word空间
四、synchronized 深入分析
把 Test.java 编译为 Test.class ,并在对应目录下执行javap -v Test.class 这个命令,你能看到对应的字节码,如下:
字节码
上图可以看到 JVM 对于同步方法和同步代码块的处理方式是不同的。
对于同步代码块:采用 monitorenter 和 monitorexit 两个指令来实现同步。
monitorenter 指令可以理解为加锁,monitorexit 可以理解为释放锁。
进入 monitorenter 指令后,线程将持有 Monitor 对象,退出 monitorenter 指令后,线程将释放该 Monitor 对象。
对于方法:出现了ACC_SYNCHRONIZED 标识。
当出现了 ACC_SYNCHRONIZED 标识符的时候,Jvm 会隐式调用 monitorenter 和 monitorexit。在执行同步方法前会调用 monitorenter,在执行完同步方法后会调用 monitorexit,释放 Monitor 对象。
你可以发现,不管是同步代码块还是同步方法,都和 Monitor 对象有关系。
那么问题又来了!!
问题:这个 Monitor 对象是啥呢?monitorenter 和 monitorexit 又是什么呢?
4.1、monitorenter
直接看 JVM 规范里对它的描述,地址在文末:
执行过程如下:
4.2、monitorexit
看 JVM 规范里对它的描述,地址在文末:
执行过程如下:
4.3、Monitor 监视器
每个对象都会关联一个 Monitor 对象,也叫做监视器。
在 HotSpot 虚拟机中,Monitor 是由 ObjectMonitor 实现的。其源码是用 c++来实现的,位于 HotSpot 虚拟机源码 ObjectMonitor.hpp 文件中(路径:src/share/vm/runtime/objectMonitor.hpp)
ObjectMonitor 主要数据结构如下:
- ObjectMonitor() {
- _header = NULL;
- _count = 0;
- _waiters = 0,
- _recursions = 0; //线程的重入次数
- _object = NULL; //存储该monitor对象
- _owner = NULL; //标识拥有该monitor的线程
- _WaitSet = NULL; //处于wait状态的线程会被加入到_WaitSet
- _WaitSetLock = 0 ;
- _Responsible = NULL ;
- _succ = NULL ;
- _cxq = NULL ; //多线程竞争锁时的单向列表
- FreeNext = NULL ;
- _EntryList = NULL ; //等待获取锁的线程,会放到这里
- _SpinFreq = 0 ;
- _SpinClock = 0 ;
- OwnerIsThread = 0 ;
- }
看到这里,我相信你就能明白为啥之前要解释对象内存布局、对象头,因为这三者之间是有对应关系的。
画图总结一下:
可以看到 ObjectMonitor 的数据结构中包含:_owner、_WaitSet 和_EntryList。
它们之间的关系转换如下:
这个过程大致就是在 JDK6 之前 实现的原理。
但是,JDK6 之前,synchronized关键字的效率是非常低的。
原因如下:
Monitor 对象是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex。
既然 Mutex Lock 涉及到底层操作系统,那这个时候就存在操作系统用户态和核心态的转换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等。
所以,在JDK 6 之后,从Jvm层面进行了优化,分为了偏向锁,轻量级锁,自旋锁,重量级锁。
五、锁升级
下面就依此来说锁是如何一步步升级的。
5.1、偏向锁
1、什么是偏向锁?
HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。
偏向锁的“偏”,就是偏心的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。
偏向锁Mark Word
不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS原子操作的性能消耗,不然就得不偿失了。
2、偏向锁原理
无锁到偏向锁的转换流程图:
偏向锁流程图
参数:-XX:+UseBiasedLocking 开启偏向锁
简单来说:
3、偏向锁的撤销
流程如下:
5.2 轻量级锁
1、什么是轻量级锁?
轻量级锁是JDK 6之中加入的锁机制,它名字中的“轻量级”是相对于使用monitor的传统锁而言的,因此传统的锁机制就称为“重量级”锁。需要强调一点的是,轻量级锁并不是用来代替重量级锁的。
引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。
2、轻量级锁原理
当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁。
流程图如下:
轻量级锁升级过程
5.3 自旋锁
1、为什么会有自旋锁?
前面聊 monitor 实现锁的时候,知道 monitor 会阻塞和唤醒线程,线程的阻塞和唤醒需要 CPU 从用户态转为核心态,频繁的阻塞和唤醒对 CPU 来说是一件负担很重的工作,这些操作给系统的并发性能带来了很大的压力。
同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间阻塞和唤醒线程并不值得。
如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个循环(自旋) , 这就是所谓的自旋锁。
2、自旋锁的优缺点
自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的。
如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。
所以,自旋等待的时间必须要有一定的限度,如果在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。
自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次,你可以使用参数 -XX : PreBlockSpin 来更改。
5.4 适应性自旋锁
在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一锁上的自选时间及锁的拥有者的状态来决定。
如果在同一个对象锁上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100次循环。
如果,对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时可能会省略掉自旋过程,避免浪费服务器处理资源。
有了自适应自旋锁,虚拟机对程序的状况预测就会变得准确,性能也会有所提升。
总结
还总结啥?说的都这么明白啦!
其实就想说可以多看看官网,比如说monitorenter和monitorexit,虽然都是英文,但是这些都是第一手资料,可以去尝试读一下,看完是真的不容易忘记。
官网地址“
1、openjdk地址:http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
2、monitorenter:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
3、monitorexit:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit
当前题目:我是这样学Synchronized关键字的
URL分享:http://www.shufengxianlan.com/qtweb/news27/9177.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联