【Java 原理剖析系列】深度 synchronized 工作原理分析
一、大致介绍
1、用过 synchronized 的童鞋都知道这个关键字是 Java 中用于解决并发情况下数据的同步访问;2、保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性;3、总的来说,其作用有三个特性:互斥性(确保线程互斥的访问同步代码)、可见性(保证共享变量的修改能够及时可见)、有序性(有效解决重排序问题);4、本章节就此和大家分享一下 synchronized 底层语义原理;
二、特性
2.1 互斥性
1、互斥性,可以认为独享的意思,每次只允许一个操作者拥有共享资源;
2、被 synchronized 修饰的代码块、实例方法、静态方法,多线程并发访问时,只能有一个线程获取到锁,其它线程都处于阻塞等待,但在此期间,这些线程仍然可以访问其它非 synchronized 修饰的方法;
2.2 可见性
1、可见性,就是每次线程的到来,都能访问到最新的值;
2、因为在互斥性的基础上,由于每次仅有一个线程执行临界区的代码,因此其修改的任何变量值对于稍后执行该临界区的线程来说是可见的;
3、再多说一句,因为互斥性的存在,也保证了临界区变量修改的原子性,而 volatile 仅仅只能保证变量修改的可见性,并不能保证原子性;
2.3 有序性
1、有序性,就是按照顺序来执行;
2、同样因为在互斥性的基础上,代码块也好,实例方法或静态方法也好,一旦被 synchronized 后,各个线程相互竞争,反正每次只能有一个线程执行;
3、打个比方,举例静态方法,TestSynchronized.java 中有个静态 synchronized static test(){ i++, j++} 方法,并且代码块被 synchronized 修饰,让 N 个线程都去调用这个方法,最后会发现每次 i 和 j 的输出值都是一样的。i++和 j++要么一起执行完,要么都不执行,不会出现先 i++后,执行了其他代码,过一会再执行 j++的情况。
三、反编译查看字节码
3.1、反编译同步代码块
1、通过 javap -verbose 反编译代码块(反编译的汇编代码就不粘贴出来了),最后会发现被反编译的代码块的前后被 monitorenter、monitorexit 一对指令包夹着;
2、关于这两条指令的作用,我们直接参考 JVM 规范中描述:
3、monitorenter 指令 JVM 规范翻译:每个对象有打自娘胎出来就自带一个内置监视器锁(monitor)。当 monitor 被占用时就会处于锁定状态,线程执行 monitorenter 指令时尝试获取 monitor 的所有权,过程如下:• 如果 monitor 的进入数为 0,则该线程进入 monitor,然后将进入数设置为 1,该线程即为 monitor 的所有者。• 如果线程已经占有该 monitor,只是重新进入,则进入 monitor 的进入数加 1.• 如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权。
4、monitorexit 指令 JVM 规范翻译:• 执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者。• 指令执行时,monitor 的进入数减 1,如果减 1 后进入数为 0,那线程退出 monitor,不再是这个 monitor 的所有者。其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权。
3.2、反编译同步方法
1、通过 javap -verbose 反编译同步方法(反编译的汇编代码就不粘贴出来了),最后会发现被反编译的同步方法附近有一个 ACC_SYNCHRONIZED 标示符;
2、关于 ACC_SYNCHRONIZED 指令的作用,我们直接参考 JVM 规范中描述:
3、ACC_SYNCHRONIZED 指令 JVM 规范翻译:• 方法级的同步是隐式的。同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。• 当某个线程要访问某个方法的时候,会检查是否有 ACC_SYNCHRONIZED,• 如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。• 这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。• 值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
3.3、反编译小结
无论是 monitorenter、 monitorexit,或者是 ACC_SYNCHRONIZED,其都是基于 Monitor 实现的,因此接下来有必要了解下 Monitor 是什么东西,在了解 Monitor 的时候,我们还有必要了解下 java 对象头。
四、Java 对象头、Monitor
4.1、Java 对象头
1、在 JVM 中,对象在内存中的布局分为三块区域:对象头、实例数据和填充数据,这个在下面图 3、图 4、图 5 都可以看到布局图,例如“实例对象(对象锁)”;
2、而 Java 对象头里面包含 Mark Word( 存储对象的 hashCode、锁信息或分代年龄或 GC 标志等信息 )、Klass Pointer( 类型指针指向对象的类元数据,JVM 通过这个指针确定该对象是哪个类的实例 )。
3、至于 Mark Word 里面具体包含了哪些信息,结构是怎么样的,请看下面图 1;
4.2、Java 对象头作用
1、Java 头对象,它实现 synchronized 的锁对象的基础;
2、特别是 Java 对象头存储的锁信息,对 synchronized 的优化起到举足轻重的作用,其中轻量级锁、偏向锁是 jdk6 对 synchronized 锁进行优化后新增加的;
3、至此,如果对 Java 对象头还不理解的话,简单粗暴的讲,我们要依赖对象头 Mark Word 中的锁信息判断来决定如何优化 synchronized;
4、如果还不理解的话,后面还会讲解到利用 Java 对象头的哪些信息是怎么做到 synchronized 锁优化的;
4.3、什么是 Monitor?
Monitor 其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象;
1、对象的所有方法都被“互斥”的执行。好比一个 Monitor 只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。
2、通常提供 singal 机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。
4.4、Monitor 粗俗理解
1、Java 对象是天生的 Monitor,每一个对象自打娘胎里出来,就带了一把看不见的锁,通常我们叫“内部锁”,或者“Monitor 锁”,或者“Intrinsic lock”。
2、Java 对象与这把内置锁紧密关联着,每个对象都存在着一个 monitor 与之关联,当锁升级为重量级锁时,监视器 Monitor 这把内置锁用来监视这些线程进入特殊的房间的,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。
3、在 Java 虚拟机(HotSpot)中,monitor 是由 ObjectMonitor 实现的,数据结构位于 HotSpot 虚拟机源码 ObjectMonitor.hpp 文件(http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/f2110083203d/src/share/vm/runtime/objectMonitor.hpp);
4.5、Java 对象头、Monitor 小结
1、讲到这里,大家对 synchronized 的原理所涉及到的一些知识点有了大概的了解,那他们究竟是如何互相作用将 synchronized 玩转的呢?
2、在 jdk6 之前,起主导作用的仍然是重量级锁,但是随着 jdk6 的问世,synchronized 添加偏向锁、轻量级锁,优化了 synchronized 锁的获取方式;
3、对于“无锁->偏向锁->轻量级锁”的转变,我们主要看线程的栈帧、Java 对象头,源码在 jdk8 的 synchronizer.cpp 中(http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/tip/src/share/vm/runtime/synchronizer.cpp);
4、对于“轻量级锁->重量级锁”,我们主要看 Monitor,源码在 jdk8 的 objectMonitor.cpp 中(http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/tip/src/share/vm/runtime/objectMonitor.cpp);
五、锁的类型
5.1、偏向锁
1、引入原由:大多数情况下,锁的竞争关系都是单一的,锁由同一个线程多次获取,为了降低获取锁的代价才引入了偏向锁,减少了一些不必要的 CAS 操作;
2、偏向锁加锁流程:
5.2、轻量级锁
1、引入原由:在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在 monitorenter 和 monitorexit 中只需要依靠一条 CAS 原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行 CAS 指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒。
2、轻量级锁加锁流程:
5.3、重量级锁
1、重量级锁就不用讲了,一直沿用至今,主要利用内置锁,内置锁的本质是依赖操作系统,因此内置锁对各个线程的阻塞是由操作系统完成(在 Linxu 下通过 pthread_mutex_lock 函数);
2、重量级锁加锁流程:
5.4、锁分类小结
1、至此,各个锁的加锁操作已经差不多了解了一番,此刻我们应该改正 monitorenter 指令就是获取对象重量级锁的错误认识,很显然,优化之后,锁的获取判断次序是偏向锁->轻量级锁->重量级锁。
2、偏向锁重要的两个方法 fast_enter、fast_exit,jdk 源码路径(http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/tip/src/share/vm/runtime/synchronizer.cpp);
3、轻量级锁重要的两个方法 slow_enter、slow_exit,jdk 源码路径(http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/tip/src/share/vm/runtime/synchronizer.cpp);
4、重量级锁重要的三个方法 enter、EnterI、exit,jdk 源码路径(http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/tip/src/share/vm/runtime/objectMonitor.cpp);
六、synchronized 锁总结图片
版权声明: 本文为 InfoQ 作者【浩宇天尚】的原创文章。
原文链接:【http://xie.infoq.cn/article/8d7b39c25e474a73d6d6e11da】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论