今天我们来聊聊Java中的锁机制
一、为什么需要锁
在单线程程序中,所有代码按顺序执行,不会出现资源竞争的问题;但在多线程并发场景下,多个线程同时访问共享资源(如全局变量、数据库连接、文件等)时,会出现竞态条件,导致数据不一致。锁是解决多线程并发访问共享资源的工具,本质是给共享资源加独占权限,保证多线程对共享资源的互斥访问,解决线程安全问题。
二、Java中常见的锁实现
Synchronized
这是 Java 提供的原生关键字。它像是一个全自动洗衣机,你只需要把衣服丢进去(加上关键字),剩下的 JVM 帮你搞定。
特点:
- 隐式锁:自动加锁/解锁,无需手动释放,即使抛异常也会释放;
- 可重入、非公平锁;
- JDK 1.6 后支持锁升级(偏向→轻量→重量);
- 独占锁(悲观锁)。
java
public class SynchronizedUsage {
// 1. 修饰实例方法:锁是当前对象(this)
public synchronized void instanceMethod() {}
// 2. 修饰静态方法:锁是当前类的Class对象
public static synchronized void staticMethod() {}
// 3. 修饰代码块:锁是指定的对象(灵活)
public void codeBlock() {
// 锁对象可以是this、Class对象、自定义对象
synchronized (SynchronizedUsage.class) {
// 临界区代码
}
}
}
ReentrantLock
它是 java.util.concurrent.locks 包下的一个类。它像是一个手动档汽车,更灵活,但也更考验车技。
特点:
- 显式锁:需手动 lock()、unlock();
- 可重入、可指定公平 / 非公平(默认非公平);
- 支持可中断锁、超时获取锁;
- 支持条件变量,可实现精准唤醒线程;
- 独占锁(悲观锁)。
java
Lock lock = new ReentrantLock();
lock.lock(); // 手动加锁
try {
// 临界区代码
} finally {
lock.unlock(); // 必须在 finally 里手动释放,否则会造成死锁
}
三、锁的分类
Java 中的锁可以从多个维度分类,这些分类并非互斥,而是从不同视角描述锁的特性。
1. 按获取锁的顺序:公平锁 vs 非公平锁
- 公平锁:多个线程按照申请锁的顺序来获取锁,缺点是频繁的队列切换会带来性能损耗;
- 非公平锁:线程获取锁时尝试直接抢占,抢锁失败再进入等待队列,性能较高,缺点是可能导致部分线程长期等待。但性能收益远大于弊端。
2. 按对待并发的态度:乐观锁 vs 悲观锁
这是一种设计思想,并非具体的锁实现。
- 悲观锁:总是假设最坏的情况,认为共享资源访问每次都会有冲突发生,所以先加锁再访问资源。适用写操作多、冲突率高的情况。
- 乐观锁:认为冲突很少发生,先访问资源,只是在提交时检查是否有冲突。适用读操作多、冲突率低的情况。
3. 按资源访问方式:独享锁 vs 共享锁
- 独享锁:同一时间,只有一个线程能持有该锁。
- 共享锁:同一时间,允许多个线程持有该锁。
4. 按是否可重复获取:可重入锁
- 可重入锁:当一个线程已经持有某个锁时,再次请求获取该锁时不会被自己阻塞,而是直接获取(锁的计数器+1)。可以避免锁嵌套导致的死锁。
5. 按锁的量级:偏向锁 → 轻量级锁 → 重量级锁
这是 synchronized 的锁升级机制,JVM 为了提升锁性能,根据线程竞争的程度来动态调整适应,锁的升级是不可逆的。
- 偏向锁:一段同步代码一直被一个线程所访问,那么该线程会自动获取锁;
- 轻量级锁:当有第二个线程尝试获取偏向锁时,偏向锁会升级成轻量级锁,其他的线程会通过自旋的形式尝试获取锁;
- 重量级锁:当锁为轻量级锁时,另一个线程自旋次数到达上限后还没有获取到锁,就会升级成重量级锁,其他申请的线程会进入阻塞。
6. 自旋锁 & 自适应自旋
自旋锁
当线程获取锁失败时,不立即阻塞挂起,而是循环重试(自旋)一段时间,看是否能获取到锁。
- 优点:避免线程内核态 / 用户态切换的开销;
- 缺点:自旋消耗 CPU 资源,若锁持有时间长,自旋会浪费 CPU。
自适应自旋
JDK 1.6 后引入,自旋次数不再固定,而是根据「前一次自旋的结果」「锁持有者的状态」动态调整:
- 若前一次自旋成功获取锁,本次自旋次数增加;
- 若前一次自旋失败,本次自旋次数减少甚至直接放弃自旋。
7. 锁优化:锁粗化 & 锁清除
这是 JVM 的自动优化手段,无需开发者手动操作。
- 锁粗化:JVM将多个连续的加锁 / 解锁操作(如循环内的同步代码)扩展到整个操作序列的外部(循环外)合并为一次;
- 锁清除:JVM通过分析,检测到某个同步锁保护的资源没有被多线程共享,则自动移除该锁。