跳到主要内容

锁机制

锁是实现线程同步的核心机制。JUC 提供了比 synchronized 更灵活、更强大的锁实现,本章将深入讲解这些锁机制。

synchronized 的局限性

在介绍 JUC 的锁之前,我们先理解为什么需要更强大的锁机制。synchronized 关键字虽然使用简单,但存在以下局限:

无法中断等待

当一个线程尝试获取 synchronized 锁时,如果锁被其他线程持有,它只能无限期等待,无法被中断。

public class SynchronizedLimitation {
private final Object lock = new Object();

public void method1() {
synchronized (lock) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public void method2() {
synchronized (lock) {
System.out.println("获取锁成功");
}
}
}

public static void main(String[] args) throws Exception {
SynchronizedLimitation demo = new SynchronizedLimitation();

Thread t1 = new Thread(() -> demo.method1());
Thread t2 = new Thread(() -> demo.method2());

t1.start();
Thread.sleep(100);
t2.start();

Thread.sleep(1000);
t2.interrupt();

System.out.println("t2 已被中断,但仍会等待锁");
t1.join();
t2.join();
}

上面的代码中,即使 t2 线程被中断,它仍然会继续等待 synchronized 锁。

无法设置超时

synchronized 不支持超时获取锁,如果锁被长时间持有,其他线程只能一直等待。

不支持公平锁

synchronized 是非公平的,新来的线程可能比等待更久的线程先获取锁,这可能导致某些线程"饥饿"。

锁的状态不可查询

无法判断锁是否被持有、有多少线程在等待等信息。

Lock 接口

JUC 的 Lock 接口定义了比 synchronized 更灵活的锁操作:

public interface Lock {
void lock(); // 获取锁
void lockInterruptibly() throws InterruptedException; // 可中断地获取锁
boolean tryLock(); // 尝试获取锁(立即返回)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 超时获取锁
void unlock(); // 释放锁
Condition newCondition(); // 创建条件变量
}

Lock 接口的核心优势:

特性synchronizedLock
中断等待不支持支持 lockInterruptibly()
超时获取不支持支持 tryLock(timeout)
公平性非公平可选公平/非公平
条件变量单一条件多个 Condition
锁状态查询不支持支持

ReentrantLock

ReentrantLock 是 Lock 接口最常用的实现,它是一种可重入锁。"可重入"意味着同一个线程可以多次获取同一把锁而不会死锁。

基本使用

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;

public void increment() {
lock.lock();
try {
count++;
System.out.println(Thread.currentThread().getName() + ": count = " + count);
} finally {
lock.unlock();
}
}

public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo = new ReentrantLockDemo();

Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
demo.increment();
}
}, "Thread-1");

Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
demo.increment();
}
}, "Thread-2");

t1.start();
t2.start();

t1.join();
t2.join();

System.out.println("最终 count = " + demo.count);
}
}

重要: lock() 和 unlock() 必须成对出现,unlock() 应该放在 finally 块中确保一定执行,否则可能导致死锁。

可重入性验证

public class ReentrantDemo {
private final ReentrantLock lock = new ReentrantLock();

public void outer() {
lock.lock();
try {
System.out.println("outer 方法获取锁");
inner();
} finally {
lock.unlock();
System.out.println("outer 方法释放锁");
}
}

public void inner() {
lock.lock();
try {
System.out.println("inner 方法获取锁(重入)");
} finally {
lock.unlock();
System.out.println("inner 方法释放锁");
}
}

public static void main(String[] args) {
ReentrantDemo demo = new ReentrantDemo();
demo.outer();
}
}

输出:

outer 方法获取锁
inner 方法获取锁(重入)
inner 方法释放锁
outer 方法释放锁

同一个线程在持有锁的情况下,可以再次获取同一把锁,这就是可重入性。

公平锁与非公平锁

ReentrantLock 支持公平锁和非公平锁两种模式:

ReentrantLock fairLock = new ReentrantLock(true);   // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁(默认)

公平锁:按照线程请求锁的顺序获取锁,先来先得。

非公平锁:允许"插队",新来的线程可能比等待队列中的线程先获取锁。

public class FairVsUnfair {
private static class Runner implements Runnable {
private final ReentrantLock lock;
private final String name;

Runner(ReentrantLock lock, String name) {
this.lock = lock;
this.name = name;
}

@Override
public void run() {
for (int i = 0; i < 3; i++) {
lock.lock();
try {
System.out.println(name + " 获取锁,第 " + (i + 1) + " 次");
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}

public static void main(String[] args) {
System.out.println("=== 非公平锁 ===");
testLock(new ReentrantLock(false));

System.out.println("\n=== 公平锁 ===");
testLock(new ReentrantLock(true));
}

private static void testLock(ReentrantLock lock) {
Thread[] threads = new Thread[3];
for (int i = 0; i < 3; i++) {
threads[i] = new Thread(new Runner(lock, "Thread-" + (i + 1)));
}

for (Thread t : threads) t.start();
for (Thread t : threads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

非公平锁的输出可能显示某个线程连续多次获取锁,而公平锁的输出会严格按照请求顺序交替获取。

性能对比:非公平锁性能通常优于公平锁,因为公平锁需要维护队列,线程切换开销更大。在大多数场景下,推荐使用非公平锁(默认)。

tryLock - 尝试获取锁

tryLock 方法提供非阻塞和超时两种获取锁的方式:

public class TryLockDemo {
private final ReentrantLock lock = new ReentrantLock();

public void tryLockExample() {
if (lock.tryLock()) {
try {
System.out.println("获取锁成功,执行任务");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁失败,执行其他任务");
}
}

public void tryLockWithTimeout() throws InterruptedException {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
System.out.println("在超时时间内获取锁成功");
} finally {
lock.unlock();
}
} else {
System.out.println("超时仍未获取锁");
}
}

public static void main(String[] args) throws InterruptedException {
TryLockDemo demo = new TryLockDemo();

Thread t1 = new Thread(() -> {
demo.lock.lock();
try {
System.out.println("t1 获取锁");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
demo.lock.unlock();
System.out.println("t1 释放锁");
}
});

Thread t2 = new Thread(() -> demo.tryLockExample());
Thread t3 = new Thread(() -> {
try {
demo.tryLockWithTimeout();
} catch (InterruptedException e) {
e.printStackTrace();
}
});

t1.start();
Thread.sleep(100);
t2.start();
t3.start();

t1.join();
t2.join();
t3.join();
}
}

tryLock 的典型应用场景:

  • 避免死锁:如果获取不到锁就放弃或执行其他逻辑
  • 限时等待:设置合理的超时时间,避免无限等待
  • 快速失败:检测锁是否可用,不可用立即返回

lockInterruptibly - 可中断获取锁

public class InterruptibleLockDemo {
private final ReentrantLock lock = new ReentrantLock();

public void holdLock() {
lock.lock();
try {
System.out.println("线程1获取锁,持有5秒");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public void interruptibleLock() {
try {
System.out.println("线程2尝试获取锁(可中断)...");
lock.lockInterruptibly();
try {
System.out.println("线程2获取锁成功");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("线程2被中断,放弃获取锁");
}
}

public static void main(String[] args) throws InterruptedException {
InterruptibleLockDemo demo = new InterruptibleLockDemo();

Thread t1 = new Thread(() -> demo.holdLock());
Thread t2 = new Thread(() -> demo.interruptibleLock());

t1.start();
Thread.sleep(100);
t2.start();

Thread.sleep(1000);
System.out.println("主线程中断 t2");
t2.interrupt();

t1.join();
t2.join();
}
}

输出:

线程1获取锁,持有5秒
线程2尝试获取锁(可中断)...
主线程中断 t2
线程2被中断,放弃获取锁
线程1释放锁

lockInterruptibly 允许在等待锁的过程中响应中断,这是与 synchronized 的重要区别。

Condition - 条件变量

Condition 提供了比 wait/notify 更灵活的线程通信机制。一个 Lock 可以关联多个 Condition,实现更精细的线程协调。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();

private final Object[] items = new Object[10];
private int putIndex, takeIndex, count;

public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
System.out.println("队列已满,生产者等待...");
notFull.await();
}
items[putIndex] = item;
if (++putIndex == items.length) putIndex = 0;
count++;
System.out.println("生产: " + item + ", 当前数量: " + count);
notEmpty.signal();
} finally {
lock.unlock();
}
}

public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
System.out.println("队列为空,消费者等待...");
notEmpty.await();
}
Object item = items[takeIndex];
if (++takeIndex == items.length) takeIndex = 0;
count--;
System.out.println("消费: " + item + ", 当前数量: " + count);
notFull.signal();
return item;
} finally {
lock.unlock();
}
}

public static void main(String[] args) {
ConditionDemo buffer = new ConditionDemo();

Thread producer = new Thread(() -> {
for (int i = 0; i < 15; i++) {
try {
buffer.put("Item-" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

Thread consumer = new Thread(() -> {
for (int i = 0; i < 15; i++) {
try {
buffer.take();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

producer.start();
consumer.start();
}
}

Condition 的核心方法:

方法说明
await()释放锁并等待,被 signal 唤醒后重新获取锁
awaitUninterruptibly()不可中断的等待
awaitNanos(long)超时等待(纳秒)
await(long, TimeUnit)超时等待
signal()唤醒一个等待线程
signalAll()唤醒所有等待线程

ReentrantReadWriteLock

ReentrantReadWriteLock 是读写锁的实现,它将锁分为读锁和写锁:

  • 读锁(共享锁):多个线程可以同时持有读锁
  • 写锁(排他锁):只有一个线程可以持有写锁,且此时不能有读锁

这种设计在读多写少的场景下能显著提高并发性能。

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

private String data = "初始数据";

public String read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始读取");
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " 读取完成: " + data);
return data;
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
readLock.unlock();
}
}

public void write(String newData) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始写入");
Thread.sleep(1000);
data = newData;
System.out.println(Thread.currentThread().getName() + " 写入完成: " + newData);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
writeLock.unlock();
}
}

public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo();

Thread r1 = new Thread(() -> demo.read(), "Reader-1");
Thread r2 = new Thread(() -> demo.read(), "Reader-2");
Thread r3 = new Thread(() -> demo.read(), "Reader-3");
Thread w1 = new Thread(() -> demo.write("新数据1"), "Writer-1");

r1.start();
r2.start();
r3.start();
w1.start();
}
}

输出显示三个读线程可以同时执行,而写线程需要等待所有读线程完成后才能执行。

读写锁的规则

当前状态读锁请求写锁请求
无锁成功成功
读锁持有成功等待
写锁持有等待等待

锁降级:写锁可以降级为读锁,但读锁不能升级为写锁。

public void lockDowngrade() {
writeLock.lock();
try {
data = "更新数据";
readLock.lock();
} finally {
writeLock.unlock();
}
try {
System.out.println("锁已降级为读锁: " + data);
} finally {
readLock.unlock();
}
}

StampedLock

StampedLock 是 Java 8 引入的锁,它提供了乐观读锁的支持,性能比 ReentrantReadWriteLock 更好。

import java.util.concurrent.locks.StampedLock;

public class StampedLockDemo {
private final StampedLock sl = new StampedLock();
private double x, y;

public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}

public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;

if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}

public void moveIfAtOrigin(double newX, double newY) {
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}

public static void main(String[] args) {
StampedLockDemo point = new StampedLockDemo();

point.move(3, 4);
System.out.println("距离原点: " + point.distanceFromOrigin());

point.moveIfAtOrigin(0, 0);
System.out.println("移动后距离: " + point.distanceFromOrigin());
}
}

StampedLock 的三种模式

模式说明
写锁排他锁,与 ReentrantReadWriteLock 类似
悲观读锁共享锁,与 ReentrantReadWriteLock 类似
乐观读锁无锁读取,通过版本号验证数据是否被修改

乐观读锁的优势在于不需要真正获取锁,只是读取数据并记录版本号,最后验证版本号是否变化。如果没有变化,说明数据一致,可以继续使用;如果变化了,再升级为悲观读锁重新读取。

锁的选择指南

场景推荐锁原因
简单同步synchronized使用简单,JVM 自动管理
需要中断/超时ReentrantLock支持可中断和超时获取
公平性要求ReentrantLock(true)支持公平锁
读多写少ReentrantReadWriteLock读锁共享,提高并发
高性能读StampedLock乐观读,性能最优
多条件等待ReentrantLock + Condition支持多个 Condition

小结

本章介绍了 JUC 提供的锁机制:

  1. Lock 接口:提供了比 synchronized 更灵活的锁操作,包括可中断、超时、公平性等特性
  2. ReentrantLock:可重入锁,是最常用的 Lock 实现,支持公平/非公平模式
  3. Condition:条件变量,提供比 wait/notify 更灵活的线程通信
  4. ReentrantReadWriteLock:读写锁,适合读多写少的场景
  5. StampedLock:Java 8 引入,支持乐观读,性能更优

选择合适的锁对于并发程序的性能至关重要。在简单场景下,synchronized 仍然是不错的选择;在需要更多控制时,ReentrantLock 提供了更大的灵活性;在读多写少的场景下,读写锁能显著提高并发性能。