目录
前言
一、 synchronized 的基本用法
二、 synchronized 的锁升级机制
1. 无锁状态
2. 偏向锁(Biased Locking)
3. 轻量级锁(Lightweight Locking)
4. 重量级锁(Heavyweight Locking)
5. 锁升级的完整流程
6. 锁升级的示例
7. 锁升级机制的意义
总结
synchronized 与 ReentrantLock 的对比
前言
synchronized
是 Java 中用于实现线程同步的关键字,它可以确保在多线程环境下,某一时刻只有一个线程能够访问特定的代码块或方法。常见的用途包括:解决共享资源访问冲突,保证数据一致性,避免竞态条件等问题。其使用方式比较简单,但背后的实现机制较为复杂,涉及到线程锁、锁的升级等概念。
一、 synchronized
的基本用法
先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表为以下3种形式;
1 修饰普通实例方法,锁的是当前实例对象
public synchronized void method() {// 需要同步执行的代码
}
- 这种方式表示对于某个实例对象(
this
),在同一时刻只有一个线程能够调用该实例的method()
方法。
2 修饰静态方法,锁的是当前类的Class对象
public synchronized static void method() {// 需要同步执行的代码
}
- 对于静态方法,锁是基于
Class
对象的,也就是锁住类本身。在同一时刻,只有一个线程能够执行类的静态方法。
3 修饰代码块,锁的是Synchonized括号里配置的对象
public void method() {synchronized (this) {// 需要同步执行的代码}
}
synchronized
还可以用于代码块中,指定一个对象作为锁。这里的this
表示当前实例对象的锁,也可以使用其他对象作为锁。
二、 synchronized
的锁升级机制
synchronized
的锁升级机制是 Java 6 引入的一项重要优化,目的是在保证线程安全的同时,尽量减少锁带来的性能开销。锁升级的过程从低到高分为四个阶段:无锁状态、偏向锁、轻量级锁 和 重量级锁。
1. 无锁状态
-
定义:对象刚被创建时,处于无锁状态。没有任何线程持有锁。
-
特点:
- 无锁状态是锁升级的起点。如果对象没有被任何线程访问,它会一直保持无锁状态。
2. 偏向锁(Biased Locking)
2.1 偏向锁的作用
-
适用场景:适用于只有一个线程访问同步代码块的场景。目标是减少无竞争时的锁开销。
-
工作原理:当第一个线程访问同步代码块时,JVM 会将对象头中的标记设置为偏向锁,并记录该线程的 ID。此时该对象的锁只偏心于该线程,因此叫偏向锁;
- 后续如果同一个线程再次访问同步代码块,JVM 会直接允许线程进入,无需加锁。
2.2 偏向锁的升级
-
触发条件:
- 当另一个线程尝试获取锁时,JVM 会检查对象头的线程 ID。
- 如果线程 ID 不匹配,说明存在竞争,偏向锁会升级为轻量级锁,同时持有该偏向锁的线程释放偏向锁。
偏向锁的撤销:JVM 会暂停持有偏向锁的线程,撤销偏向锁,并将对象头恢复为无锁状态或轻量级锁状态。
2.3 偏向锁的优点
- 减少无竞争时的开销:在单线程访问的场景下,偏向锁几乎没有任何性能开销。
3. 轻量级锁(Lightweight Locking)
3.1 轻量级锁的作用
-
适用场景:
- 适用于多个线程交替访问同步代码块,但没有真正竞争的场景。
- 目标是减少线程阻塞的开销。
-
工作原理:
- 当线程尝试获取锁时,JVM 会在当前线程的栈帧中创建一个锁记录(Lock Record),并将对象头中的标记复制到锁记录中。
- 然后,JVM 会尝试通过 自旋CAS 操作将对象头中的标记替换为指向锁记录的指针。
- 如果 自旋CAS 成功,线程获取锁。
- 如果 自旋CAS失败,说明存在竞争,轻量级锁会升级为重量级锁。
3.2 轻量级锁的升级
- 触发条件:
- 当存在多个线程竞争锁时,自旋CAS 操作就可能会失败。
- 当CAS失败时,JVM 会将轻量级锁升级为重量级锁。
3.3 轻量级锁的优点
- 减少线程阻塞:在低竞争场景下,轻量级锁通过 自旋CAS 操作避免了线程阻塞,性能较高。
4. 重量级锁(Heavyweight Locking)
4.1 重量级锁的作用
-
适用场景:
- 适用于高竞争场景,即多个线程同时竞争锁。目标是保证线程安全。
-
工作原理:
- 重量级锁依赖于操作系统的互斥信号量(Mutex)来实现线程同步。
- 当线程尝试获取锁时,如果锁已被其他线程持有,当前线程会进入阻塞状态,等待锁释放。
4.2 重量级锁的缺点
- 性能开销大:线程阻塞和唤醒会导致上下文切换,性能开销较大。
5. 锁升级的完整流程
-
初始状态:对象处于无锁状态。
-
偏向锁:
- 第一个线程访问同步代码块时,JVM 将对象标记为偏向锁,并记录偏向线程的 ID。
- 如果该线程再次访问同步代码块,JVM 会直接允许线程进入。
-
轻量级锁:
- 当另一个线程尝试获取锁时,JVM 会撤销偏向锁,升级为轻量级锁。
- 线程通过 自旋CAS 操作竞争锁。
- 如果 CAS 成功,线程获取锁。
- 如果 CAS 失败,说明存在竞争,升级为重量级锁。
-
重量级锁:
- 当多个线程竞争锁时,轻量级锁会升级为重量级锁。
- 线程进入阻塞状态,等待锁释放。
6. 锁升级的示例
以下是一个简单的示例,演示锁升级的过程:
public class LockUpgradeExample {private static final Object lock = new Object();public static void main(String[] args) {// 线程 1:获取锁new Thread(() -> {synchronized (lock) {System.out.println("Thread 1: 获取锁");try {Thread.sleep(2000); // 模拟操作} catch (InterruptedException e) {e.printStackTrace();}}}).start();// 线程 2:尝试获取锁new Thread(() -> {try {Thread.sleep(500); // 确保线程 1 先获取锁} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock) {System.out.println("Thread 2: 获取锁");}}).start();}
}
输出结果:
Thread 1: 获取锁
Thread 2: 获取锁
锁升级过程:
- 线程 1 获取锁,对象从无锁状态升级为偏向锁。
- 线程 2 尝试获取锁,偏向锁升级为轻量级锁。
- 如果线程 1 和线程 2 同时竞争锁,轻量级锁会升级为重量级锁。
7. 锁升级机制的意义
-
偏向锁:在单线程访问的场景下,偏向锁几乎没有任何性能开销。
-
轻量级锁:在低竞争场景下,轻量级锁通过 CAS 操作避免了线程阻塞,性能较高。
-
重量级锁:在高竞争场景下,重量级锁通过阻塞线程来保证线程安全。
总结
synchronized
的锁升级机制是 Java 并发编程中的重要优化,它通过偏向锁、轻量级锁 和 重量级锁 的逐步升级,在保证线程安全的同时,尽量减少锁带来的性能开销。开发者可以根据具体的并发场景,理解锁升级的原理,从而编写出更高效的并发程序。
synchronized
与 ReentrantLock
的对比
特性 | synchronized | ReentrantLock |
---|---|---|
实现方式 | JVM 内置实现 | JDK 实现 |
锁类型 | 非公平锁 | 可选择公平锁或非公平锁 |
可中断性 | 不支持 | 支持 |
超时机制 | 不支持 | 支持 |
条件变量 | 不支持 | 支持 |
锁升级 | 支持(偏向锁、轻量级锁、重量级锁) | 不支持 |
性能 | 低竞争场景下性能较好 | 高竞争场景下性能较好 |