09. 小伙子, 听说你对synchronized理解很深?..
大佬: 你这段代码有线程安全问题, 解决下!
我: ……
大佬: Synchronized加上, 别锁方法, 锁必要的代码块就好, 这样性能高点.
我: 哦, 好的…………(ps, 迅速百度……)
前言
Synchronized(也叫同步)是 Java 中解决并发问题的一种最常用的方法.
在 JDK1.5 之前, synchronized 是一个重量级锁
, 相对于j.u.c.Lock
,显得很笨重.
随着 Javs SE 1.6 对 synchronized 进行的各种优化, synchronized 再次焕发了生机.
下面, 我们一起探索 synchronized 的基本使用及原理
基本使用
前面我们讲过, synchronized 可以保证线程安全.
它的作用主要有三个
- 原子性
确保线程“
互斥
”的访问同步代码
- 可见性
保证
共享变量
的修改能够及时可见,
主要通过Java内存模型中的以下特征来保证
“
对一个变量unlock
操作之前,必须要同步到主内存中;
如果对一个变量进行lock
操作,则将会清空工作内存中此变量的值,
在执行引擎使用此变量前,需要重新从主内存中load
操作或assign
操作初始化变量值
”
- 有序性
解决
重排序
问题,
即“一个unlock
操作先行发生(happen-before)于后面对同一个锁的lock
操作”
从语法上讲,synchronized 可以把任何一个非 null 对象作为"锁"
,
在 HotSpot JVM (使用最广泛的java虚拟机)实现中,
锁有个专门的名字: 对象监视器(Object Monitor)
.
synchronized有三种方式来加锁, 分别是:
-
方法锁 synchronized void method()
-
对象锁 synchronized(this)
-
类锁 synchronized(Demo.Class)
其中在方法锁
层面可以有如下3种方式:
-
修饰
非静态方法
时,监视器锁便是对象实例(this)
; -
修饰
静态方法
时,监视器锁便是对象的 Class 实例
,因为 Class 数据存在于永久代,因此静态方法锁相当于该类的一个全局锁; -
修饰某一个
对象实例
时, 监视器锁便是括号括起来的对象实例
;
注意:
synchronized内置锁是一种对象锁(锁的是对象而非引用变量
),作用粒度是对象 ,可以用来实现对临界资源的同步互斥
访问,是可重入
的, 其可重入最大的作用是避免死锁.
如子类同步方法调用了父类同步方法,如没有可重入的特性,则会发生死锁;
synchronized同步原理
数据同步需要依赖锁,那锁的同步又依赖谁?
synchronized 是在软件层面依赖 JVM(基于JVM的内置锁Monitor实现);而 j.u.c.Lock 是在硬件层面依赖特殊的 CPU 指令.
我们都知道, synchronized的使用遵循以下规则:
当一个线程访问同步代码块时,首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁,
那么它是如何来实现这个机制的呢?
对象锁: synchronized (this)
我们先看一段简单的代码:
public class Test {
public void method() {
synchronized (this) {
System.out.println("Hello World!");
}
}
}
编译
javac Test.java
反汇编
javap -v Test.class

下面, 我们针对代码的重要指令进行说明
monitorenter
每个对象都是一个监视器锁(monitor).
当 monitor 被占用时就会处于锁定状态,
线程执行 monitorenter 指令时会尝试获取 monitor 的所有权
过程如下:
- 如果 monitor 的进入数为 0,则该线程进入 monitor,然后将进入数设置为 1,该线程即为 monitor 的所有者;
- 如果线程已经占有该 monitor,只是重新进入,则进入 monitor 的进入数加 1;
- 如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权;
monitorexit
执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者.
指令执行时,monitor 的进入数减 1,
如果减 1 后进入数为 0,那线程退出 monitor,
不再是这个 monitor 的所有者.
其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权.
细心的朋友会在截图中发现
不对啊, 指令中出现了两次monitorexit?
其实是这样的:
正常情况下, 同步代码块会调用第一个monitorexit释放锁;
如果同步代码块中出现异常,则调用第二个monitorexit来保证释放锁;
通过上面两段描述,我们应该能很清楚的看出 synchronized 的实现原理,
synchronized 的语义底层是通过一个 monitor 的对象来完成.
其实 wait/notify 等方法也依赖于 monitor 对象,
这就是为什么只有在同步的块或者方法中才能调用 wait/notify 等方法,
否则会抛出java.lang.IllegalMonitorStateException
的异常的原因.
方法锁 synchronized void method()
再来看一下同步方法
public class Test {
public synchronized void method() {
System.out.println("Hello World!");
}
}
反汇编

从编译的结果来看,
方法的同步 并没有通过指令 monitorenter
和 monitorexit
来完成(理论上也可以通过这两条指令来实现),
不过相对于普通方法, 其常量池中多了 ACC_SYNCHRONIZED
标示符.
JVM 就是根据该标示符来实现方法的同步的:
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,
如果设置了,执行线程将先获取 monitor,
获取成功之后才能执行方法体,
方法执行完后再释放 monitor.
在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象.
synchronized(this) 和 synchronized void method() 两种同步方式本质上没有区别,
只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成.
而monitorenter
和 monitorexit
两个指令的执行是 JVM 通过调用操作系统的互斥原语 mutex 来实现的, 被阻塞的线程会被挂起,等待重新调度, 这样会导致“用户态和内核态
”来回切换, 对性能有较大影响.
欢迎关注我
技术公众号 “CTO技术”