synchronized 的三种应用方式
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码前要获得给定对象的锁。
实现代码如下:
public class SynchronizedDemo {
private Object lock1 = new Object();
private static Object lock2 = new Object();
/**
* 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
*/
public synchronized void demo1() {
}
/**
* 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
*/
public synchronized static void demo2() {
}
/**
* 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码前要获得给定对象的锁
*/
public void demo3() {
// 作用于当前实例加锁
synchronized (this) {
}
//作用于当前类对象加锁
synchronized (SynchronizedDemo.class) {
}
// 作用于lock1实例加锁
synchronized (lock1) {
}
// 作用于当前类对象加锁
synchronized (lock2) {
}
}
}
synchronized 扩后后面的对象是一把锁,在 Java 中任意一个对象都可以成为锁,简单来说,我们把 Object 比喻是一个 key,拥有这个 key 的线程才能执行这个方法,拿到这个 key 以后在执行方法过程中,这个 key 是随身携带的,并且只有一把。如果后续的线程想访问当前方法,因为没有 key 所以不能访问只能在门口等着,等之前的线程把 key 放回去。所以,synchronized 锁定的对象必须是同一个,如果是不同对象,就意味着是不同的房间的钥匙,对于访问者来说是没有任何影响的。
synchronized 锁的原理
字节码指令
通过 javap -v 来查看对应代码的字节码指令,对于同步块的实现使用了 monitorenter 和 monitorexit 指令,前面我们在讲 JMM 的时候,提到过这两个指令,他们隐式的执行了 Lock 和 UnLock 操作,用于提供原子性保证。
monitorenter 指令插入到同步代码块开始的位置,monitorexit 指令插入到同步代码块结束位置,jvm 需要保证每个monitorenter 都有一个 monitorexit 对应。
这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由 synchronized 所保护对象的监视器。
线程执行到 monitorenter 指令时,会尝试获取对象所对应的 monitor 所有权,也就是尝试获取对象的锁,而执行monitorexit,就是释放 monitor 的所有权。
查看如下代码的字节码指令:
public class SynchronizedDemo {
public void demo3() {
synchronized (this) {
}
}
public static void main(String[] args) {
}
}
为什么这里会出现两个 monitorexit 呢?因为一个线程获得了一把锁就必须要释放,否则其他线程就不能获得锁,导致其他线程无限等待。所以当方法运行时出现异常了,也需要提供释放锁的指令(第二个 monitorexit)。
对象头
在 Hotspot 虚拟机中,对象在内存中的布局分为三个区域:对象头、实例数据、对齐填充。Java 对象头是实现 synchronized 锁对象的基础,synchronized 使用的锁对象存储在 Java 对象头里。它是轻量级锁和偏向锁的关键。
Java 对象头的内容如下:
Mark Word
Mark Work 默认存储对象的 HashCode、分代年龄和锁标记位。32 位 JVM 的 Mark Work 的默认存储结构如下表所示。
在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化。Mark Word 可能变化为存储以下4种数据。如下表所示。
在64位虚拟机下,Mark Word 是64 bit 大小的,其存储结构如下表所示。
源码实现
如果想更深入了解对象头在 JVM 源码中的定义,需要关心几个文件,oop.hpp / markOop.hpp。每个 Java Object 在 JVM 内部都有一个 oop / oopDesc 与之对应。先在 oop.hpp 中看 oopDesc 的定义。
这个 _mark 属性就是上面说到的 Mark Word,在 markOop.hpp 文件中有一些注释说明了 markOop 的内存布局。
对象监视器
什么是 Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制。所有的 Java 对象是天生的 Monitor,每个 Object 的 markOop.hpp -> monitor() 方法返回 ObjectMonitor 对象。
- oop.hpp 中 oopDesc 类是 JVM 对象的顶级基类,所以每个 Object 对象都包含 markOop。
- markOop.hpp 中 markOopDesc 继承自 oopDesc,并扩展了自己的 monitor() 方法,这个方法返回一个ObjectMonitor 指针对象。
- objectMonitor.hpp 在 hotspot 虚拟机中,采用 ObjectMonitor 类来实现 monitor。
锁的升级和获取过程
synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 synchronized 效率低的原因。
因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”。JDK中对 synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。
偏向锁
偏向锁适用的场景是同一线程多次执行同步块的情况,如果存在多个线程的情况,就会导致偏向锁膨胀为轻量级锁。
偏向锁加锁
当一个线程访问同步块并获取锁时,将通过 CAS(Compare And Swap) 来尝试将对象头中的 Thread ID 字段设置为自己的线程号,如果设置成功,则获得锁,那么以后线程再次进入和退出同步块时,就不需要使用 CAS 来获取锁,只是简单的测试一个对象头中的 Mark Word 字段中是否存储 着指向当前线程的偏向锁;如果使用 CAS 设置失败时,说明存在锁的竞争,那么将执行偏向锁的撤销操作(revoke bias),将偏向锁升级为轻量级锁。
偏向锁撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。
下图线程1演示了偏向锁初始化的流程,线程2演示了偏向锁撤销的流程。
关闭偏向锁
偏向锁在 Java 6 和 Java 7 里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用 JVM 参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
轻量级锁
轻量级锁适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
轻量级锁加锁
线程在执行同步块之前,JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中,官方称为 Displaced Mark Word。然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。当竞争线程的自旋次数达到界限值(threshold),轻量级锁将会膨胀为重量级锁。
轻量级锁解锁
轻量级解锁时,会使用原子的 CAS 操作将 Displaced Mark Word 替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
下图两个线程同时争夺锁,导致锁膨胀的流程图
重量级锁
重量级锁通过对象内部的监视器(Monitor)实现,其中 Monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
在 hotspot 虚拟机中,通过 ObjectMonitor 类来实现 Monitor。锁的获取过程如下图所示。
参考
https://www.zhihu.com/question/55075763
https://www.cnblogs.com/paddix/p/5405678.html
synchronized 锁升级机制
Java并发之彻底搞懂偏向锁升级为轻量级锁
JVM锁简介:偏向锁、轻量级锁和重量级锁
死磕系列,估计比较牛,需要死磕的可以看看
这些文章内容之间有可能有冲突,挑着看看吧。
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/og00ra 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。