回答
synchronized
是 Java 中一个重量级的关键字,它用于实现线程同步,确保多线程环境下对共享资源的安全访问。它可以修饰方法或代码块,保证一次只有一个线程可以执行同步方法或代码块内的代码。
synchronized
有两种形式上锁,一个是对方法上锁,一个是对代码块上锁。其实他们底层实现原理都是一样的。在进入同步代码之前先获取锁,锁计数 + 1,执行完同步代码后释放锁,锁计数 -1,如果获取失败就阻塞式等待锁的释放。他们的不同之处在于他们在同步块的识别方式有所不同。
当一个方法被 synchronized
修饰时,它的方法标志中会包含 ACC_SYNCHRONIZED
标志。当某个线程要访问方法时,会首先检查是否有 ACC_SYNCHRONIZED
设置,如果有,则需要先获取监视器锁,获取成功后才能执行方法,方法执行完成后再释放监视器锁。如果在该线程执行同步方法期间,有其他线程来请求执行方法,会因为无法获取监视器锁而阻塞。
而同步代码块则是使用 monitorenter 和 monitorexit 指令来实现的。我们可以理解执行 monitorenter 为加锁,执行 monitorexit 为释放锁。每个对象都维护着一个锁的计数器,为被锁定的对象该计数器为 0。当一个线程在执行 monitorenter 之前需要尝试后去锁,如果这个对象没有被锁定,或者当前线程已经拥有了该对象的锁,那么这把锁的计数器 + 1,当执行monitorexit指令时,锁的计数器也会减1。
扩展
基本使用
Synchronized
是 Java 中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:
- 原子性:确保线程互斥的访问同步代码
- 可见性:保证共享变量的修改能够及时可见。
- 有序性:有效解决重排序问题。
从语法上讲,Synchronized
可以把任何一个非null对象作为 “锁”,在HotSpot JVM实现中,这个锁有个专门的名字:对象监视器(Object Monitor)。
从语法上讲,synchronized
总共有三种用法,三种用法锁定的对象都不同:
- 当
synchronized
用于实例方法时,监视器锁(monitor)便是对象实例(this
)。
public synchronized void method() {
// 方法体
}
锁定的是调用该方法的对象实例。
- 当
Synchronized
用于静态方法时,监视器锁(monitor)便是对象的Class实例。
public static synchronized void staticMethod() {
// 方法体
}
锁定的是这个类的所有对象。
- 当
Synchronized
用于同步代码块时,监视器锁(monitor)便是括号括起来的对象实例。
public void method() {
synchronized (object) {
// 代码块
}
}
同步实现原理
同步需要依赖锁,那锁的同步又依赖谁?synchronized 给出的答案是在软件层面依赖JVM。我们来看同步方法和同步代码块是如何实现的。
同步方法
代码如下:
public class SynchronizedTest {
public synchronized void test() {
System.out.println("死磕 Java 面试...");
}
// 为了对比
public void test1() {
System.out.println("死磕 Java 面试...");
}
}
使用 javac SynchronizedTest.java
,编译 Java 文件为 .class 文件,然后使用 javap -verbose SynchronizedTest
,结果如下:
对于普通方法,其实就是常量池中多了 ACC_SYNCHRONIZED
标识符,JVM就是根据该标示符来实现方法的同步的:
当方法被调用时,调用指令将会检查方法的
ACC_SYNCHRONIZED
访问标志是否被设置,如果设置了,执行线程将先获取 monitor ,获取成功之后才能执行方法体,方法执行完后再释放 monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
同步代码块
代码如下:
public class SynchronizedTest {
public void test() {
synchronized (this) {
System.out.println("死磕 Java 面试...");
}
}
}
反编译 .class 文件,内容如下:
如上面所提到的执行 monitorenter
为加锁,执行 monitorexit
为释放锁。
每个对象都是一个监视器锁。当monitor被占用时就会处于锁定状态。
执行 monitorenter
过程如下:
- 如果 monitor 的计数为 0,说明锁未被持有,JVM 将锁分配给执行
monitorenter
的线程,并将 monitor 的计数设置为 1。 - 如果线程已经占有了该 monitor,当前线程重入,monitor 的计数 + 1。
- 如果 monitor 被其他线程持有,那么当前线程将被阻塞,直到锁被释放。
执行 monitorexit
的过程:
- 执行
monitorexit
的线程必须是对应 monitor 的所有者,即执行monitorexit
和monitorenter
要是同一个线程。 - 执行
monitorexit
时,monitor 的计数器 - 1,如果计数器大于 0,表示当前线程还持有 monitor(可重入),锁不会被释放,如果计数器等于 0,表示当前线程不再持有 monitor ,锁被释放。 - monitorexit 指令出现两次,原因是为了兼顾执行同步代码时出现异常而导致锁无法释放的问题。所以第1次为同步正常退出释放锁,第2次为发生异步退出释放锁。
监视器(monitor)
synchronized
在 JVM 中的实现都是基于进入和退出monitor对象来实现方法同步和代码块同步,所以我们有必要来了解下 monitor。
那什么是 monitor 呢?我们可以把它理解为一种同步机制,它通常被描述为一个对象。
我们知道,在 Java 中一切皆对象,同理,在 Java 中所有的 Java 对象是天生的 monitor,每一个 Java 对象都有成为 monitor 的可能。这是因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁**,它叫内置锁**。
monitor 由 ObjectMonitor 实现,其主要数据结构如下:
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
几个重要属性如下:
_header
:指向对象头,在 Java 中,每个对象都有一个对象头,其中包含了与锁和垃圾收集相关的信息。_count
:用于记录重入锁的数量。在 Java 中,同一个线程可以多次获得同一个锁(即重入锁),这个字段就是用来记录该线程获取锁的次数。_recursions
:用于记录同一个线程重复获取这个锁的次数。_waiters
:记录正在等待获取这个对象锁的线程数量。_owner
:当前拥有这个 monitor 的线程_WaitSet
:处于 wait 状态的线程,会被加入到_WaitSet
中,可通过notify()
或notifyAll()
唤醒。_EntryList
:处于等待锁 block 状态的线程,会被加入到_EntryList
中。
ObjectMonitor 中有两个队列,_WaitSet
和 _EntryList
,用来保存 ObjectWaiter 对象列表( 每个等待锁的线程都会被封装成 ObjectWaiter对象 ),_owner
指向持有ObjectMonitor对象的线程,当多个线程访问一段同步代码时:
- 首先会进入
_EntryList
集合,当线程获取到对象的 monitor 后,进入_owner
区域并把 monitor 中的_owner
变量设置为当前线程,同时monitor中的计数器_count
加1; - 若持有 monitor 的线程调用
wait()
,将释放当前持有的monitor,_owner
变量恢复为null,_count
自减1,同时该线程进入_WaitSet
集合中等待被唤醒; - 若持有 monitor 的线程执行完毕,也将释放当前持有的 monitor,并复位变量的值,以便其他线程进入获取monitor;