Synchronized 是什么?
sychronied是Java中并发编程的重要关键字之一。在并发编程中synchronized一直是解决线程安全问题,它可以保证原子性,可见性,以及有序性。
- 原子性 :保证这段代码不会被并发打断
synchronized 通过 monitorenter / monitorexit 实现互斥。同一时刻只有一个线程能进入同一把锁保护的临界区,因此临界区内的复合操作整体对外表现为不可分割。同一个锁,同一时刻只能被一个线程持有。因此临界区代码不会被并发打断。 - 可见性 :释放锁前的修改,对之后获取同一把锁的线程可见
JMM 中有非常重要的 Happens-Before 规则:: 对一个锁的解锁 happens-before 于后续对同一个锁的加锁。
synchronized 在释放锁时,会保证此前的写操作对之后获取同一锁的线程可见;线程重新获得锁后,能够看到前一个线程释放锁前的修改。 - 有序性 : 禁止重排序破坏同步语义
CPU 和编译器为了优化性能,可能会进行指令重排序 。在不影响单线程执行结果的情况下,底层有可能调整某些指令执行顺序。
允许不影响程序语义的优化和重排序,但不能发生破坏 synchronized 同步语义、Happens-Before 关系的重排序。
即: synchronized 会建立明确的同步边界,编译器和 CPU 不能进行那种破坏锁的 Happens-Before 关系和程序同步语义的重排序。
text
线程A临界区操作 // 先于 线程A unlock
↓
unlock(lock)
↓
Happens-Before
↓
lock(lock)
↓
线程B临界区操作 // 晚于 线程B lock
synchronized 可以保证原子性、可见性和有序性。原子性来自锁的互斥机制,同一把锁同一时刻只能被一个线程持有,因此同步代码块不会被多个线程同时执行。可见性来自 JMM 的 Happens-Before 规则,一个线程对锁的释放 Happens-Before 于另一个线程随后对同一把锁的获取,因此前一个线程的修改对后一个线程可见。有序性则是通过同步语义限制编译器和 CPU 的指令重排序,不能让重排序破坏锁建立的 Happens-Before 关系。
Synchronized 使用
synchronized 可以修饰方法,也能够使用 synchronized(obj){} 定义同步代码块。
- 修饰方法
- 修饰实例方法
java
public synchronized void test() {
// ...
}
锁住的是当前实例对象 this。等价于
java
public void test() { synchronized (this) {
// ...
}
}
- 修饰静态方法
java
public static synchronized void test() {
// ...
}
锁住的是当前类对应的 Class 对象,等价于:
java
public static void test() {
synchronized (Test.class) {
// ...
}
}
由于一个类通常只有一个对应的 Class 对象,因此无论通过哪个实例调用,竞争的都是同一把类锁。
- 修饰代码块
java
synchronized (obj) {
// ...
}
锁住的是指定的对象 obj。
线程进入同步代码块之前,必须先获得 obj 对应的锁;退出代码块后释放锁。
三种方式对比
| 写法 | 加锁对象 |
|---|---|
synchronized 实例方法 |
当前实例 this |
static synchronized 静态方法 |
当前类的 Class 对象,如 Test.class |
synchronized(obj) |
指定的 obj 对象 |
使用
sychronied修饰普通方法和静态方法,其实也等价于synchronized(this){}与synchronized(class){}。
Synchronized 锁升级机制
在 JDK 6 之前,Synchronized 的实现是 重量级锁。
synchronized 的锁升级是 JDK 6 之后对 synchronized 的优化,核心目的是:在无竞争或轻竞争时,尽量避免直接使用重量级锁,减少用户态和内核态切换的开销。
java 对象在 虚拟机内存中的结构:
text
Java 对象 = 对象头 + 实例数据 + 对齐填充
对象头 = Mark Word + Klass Pointer + Array Length(仅数组对象有)
Mark Word 存储对象自身的运行时数据,32位 四字节,64位 八字节;
Klass Pointer 指向方法区中该对象的类元数据,32 位 四字节,64位 开启压缩指针(默认)四字节 否则,八字节;
Array Length 数组对象的长度,占 4 字节。
锁状态主要记录在 Java 对象头的 Mark Word 中,升级过程通常是:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
一般认为是不可逆的,但偏向锁可以被撤销。JDK 15 之后偏向锁默认禁用,新版本中逐步移除;如果面试官问 JDK 8,则默认开启。
以32位 JVM 为例,Mark Word 如下:

锁升级流程
text
第一次进入 synchronized
↓
无锁
↓
偏向锁
↓
出现其他线程竞争
↓
轻量级锁
↓
竞争进一步加剧
↓
重量级锁
1. 无锁
对象刚创建时是无锁状态。
Mark Word 中存 hashCode、GC 年龄、锁标志位等。
以 32 位为例:锁标志位是 01,偏向位是 0。
当线程第一次 进入 synchronized, 会将锁偏向该线程,锁成为偏向锁。
2. 偏向锁
首先之所以会引入偏向锁是因为:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的操作。
在程序进入同步代码块时,会访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,若为偏向锁状态,则查看偏向锁状态下线程ID是否指向当前线程 。如果是则直接执行同步代码。
但是mark word中记录的线程ID如果不是当前线程,则通过CAS比较与交换尝试修改对象头获得锁。CAS操作成功则可以直接执行同步代码,否则表示有其他线程竞争,此时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁 ,然后被阻塞的线程继续往下执行同步代码。
**适用场景:**只有一个线程反复进入同步块。
3. 轻量级锁
轻量级锁状态下,代码进入同步块时,如果同步对象锁状态为无锁状态, 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝 ,接着虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针 。这个操作如果成功则代表获取到了锁,但是如果失败,则会检查对象Mark Word是不是指向当前线程栈帧中的锁记录,如果是,则说明本身当前线程就拥有此对象的锁,就可以直接执行同步代码。否则说明锁对象被其他线程获取,当前线程是竞争者,那么当前线程会自旋等待锁,也就是不断重试,当重试一定次数后,总不能一直重试下去吧,太耗CPU了。所以这时候就要升级为重量级锁。
**适用场景:**少量线程交替执行,竞争不激烈。
4. 重量级锁
重量级锁就是通过对象监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
**适用场景:**多线程激烈竞争。