一句话速通:
Java 锁机制核心是 synchronized 和 ReentrantLock,两者都是可重入锁;
JDK 1.6+ 后 synchronized 引入锁升级(无锁➡️偏向锁➡️轻量级锁➡️重量级锁),性能大幅提升;
简单场景优先用 synchronized,需要高级功能(比如可中断、可超时、公平锁)时用 ReentrantLock。
为什么需要锁?
在并发场景下,多个线程同时修改同一个共享变量,会出现线程安全问题。
举个例子说明一下:
java
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 这行代码不是原子操作!
}
public int getCount() {
return count;
}
}
如果1000个线程同时调用 increment(),最终 count 的值很可能小于1000。
因为 count++ 不是原子操作,它分为读取count 、count+1 、写回count三步,多线程同时执行会互相覆盖。
锁的作用就是把这段代码加锁,让同一时间只有一个线程能执行,从而保证操作的原子性。
synchronized
synchronized 是JVM内置的关键字,不需要手动释放锁。
| 用法 | 锁的对象 | 示例 |
|---|---|---|
| 锁实例方法 | 锁当前对象 this |
public synchronized void increment() |
| 锁静态方法 | 锁当前类的 Class 对象 |
public static synchronized void increment() |
| 锁代码块 | 锁指定的对象 | synchronized (lock) { ... } |
可以用synchronized给之前的例子上锁:
java
public class SafeCounter {
private int count = 0;
// 锁实例方法:同一时间只有一个线程能执行
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
ReentrantLock
ReentrantLock 是 java.util.concurrent(JUC)包下的锁,是基于代码实现的锁,比 synchronized 更灵活,但需要手动释放锁。
也可以用ReentrantLock给之前的例子上锁:
java
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockCounter {
private int count = 0;
// 创建 ReentrantLock 实例
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 加锁
try {
count++;
} finally {
lock.unlock(); // 必须在 finally 里释放锁,防止死锁
}
}
public int getCount() {
return count;
}
}
ReentrantLock 有3个 synchronized 没有的功能:
| 功能 | 说明 | 示例 |
|---|---|---|
| 可中断 | 线程在等待锁的过程中,可以被中断,停止等待 | lock.lockInterruptibly() |
| 可超时 | 尝试获取锁,等待一段时间后如果还没拿到,就放弃 | lock.tryLock(1, TimeUnit.SECONDS) |
| 公平锁 | 按线程请求锁的顺序分配锁(先来先得),默认是非公平锁 | new ReentrantLock(true) |
可重入锁
synchronized 和 ReentrantLock 都是可重入锁。
那什么是可重入呢?
可重入的意思就是同一个线程,可以多次获取同一把锁,不会被自己阻塞。
那又为什么需要可重入呢?
举个递归调用的例子:
java
public class ReentrantExample {
// synchronized 是可重入的
public synchronized void methodA() {
System.out.println("执行 methodA");
methodB(); // 调用 methodB,methodB 也需要同一把锁
}
public synchronized void methodB() {
System.out.println("执行 methodB");
}
public static void main(String[] args) {
ReentrantExample example = new ReentrantExample();
example.methodA();
}
}
如果锁是不可重入的,那么线程在执行 methodA() 时已经拿到了锁,调用 methodB() 时会因为拿不到锁而被自己阻塞,导致死锁。
synchronized 的锁升级
在 JDK 1.6 之前,synchronized 是重量级锁,性能很差;
JDK 1.6 之后,引入了锁升级机制,提升了 synchronized 的性能。
锁升级的意思是:
随着多线程竞争的加剧,锁会从【无锁】➡️【偏向锁】➡️【轻量级锁】➡️【重量级锁】逐步升级,而且升级是单向的,只能升不能降。
Java对象在内存中分为3部分:
对象头、实例数据、对齐填充,其中对象头里的 Mark Word 会记录锁的状态。
而synchronized 的锁是存放在【对象头】里的。
Mark Word 的结构(32 位 JVM):
| 锁状态 | 25位 | 4位 | 1位(是否偏向锁) | 2位(锁标志位) |
|---|---|---|---|---|
| 无锁 | 对象的哈希码 | 分代年龄 | 0 | 01 |
| 偏向锁 | 偏向线程ID | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | ||
| 重量级锁 | 指向重量级锁(monitor)的指针 | 10 |
锁升级的四个阶段
1. 无锁
没有线程竞争锁,对象处于无锁状态。
2. 偏向锁
只有一个线程多次获取同一把锁,没有其他线程竞争。
第一个线程获取锁时,会在对象头的 Mark Word 里记录【偏向线程ID】,这个线程以后再次获取锁时,只需要检查偏向线程ID是不是自己,如果是,直接获取锁。
当有第二个线程来竞争这把锁时,偏向锁会撤销,升级为轻量级锁。
3. 轻量级锁
多个线程交替获取锁,但是竞争不太激烈(比如线程A获取锁,执行完释放了,线程B再获取)。
线程在自己的栈帧里创建一个锁记录(Lock Record),用 CAS(Compare And Swap,比较并交换)操作,尝试把对象头的 Mark Word 替换成指向自己栈中锁记录的指针。
如果 CAS 成功,就获取到了轻量级锁。
当有多个线程同时竞争锁,CAS 失败多次,就会升级为重量级锁。
4. 重量级锁
多个线程同时竞争锁,竞争激烈。
锁升级为重量级锁后,对象头的 Mark Word 会指向一个monitor(监视器)对象。
没有获取到锁的线程会进入阻塞队列,被操作系统挂起,等待持有锁的线程释放锁后唤醒。