在 Java 里,Lock
接口是自 JDK 1.5 起引入的用于实现线程同步的工具,它为开发者提供了比synchronized
关键字更为灵活的锁操作方式。下面为你详细介绍Lock
相关的知识和具体用法:
1. 核心接口与实现类
- Lock 接口:定义了锁操作的基本方法,像获取锁(
lock()
)、尝试获取锁(tryLock()
)以及释放锁(unlock()
)等。 - ReentrantLock:可重入锁的实现,支持公平锁和非公平锁两种模式。
- ReentrantReadWriteLock:读写锁实现,能分离读锁和写锁,读锁可被多个线程共享,而写锁具有排他性。
- StampedLock:JDK 8 新增的锁,支持乐观读锁、悲观读锁和写锁三种模式。
2. 相较于 synchronized 的优势
- 灵活的锁获取方式:可以使用
tryLock()
尝试获取锁,还能设置超时时间。 - 可中断的锁获取:通过
lockInterruptibly()
方法,在等待锁的过程中可以中断线程。 - 条件变量支持:借助
newCondition()
方法生成条件对象,实现更精细的线程等待与唤醒操作。 - 公平锁选项:能够选择公平锁,保证线程获取锁的顺序性。
3. 基础用法示例
(1)ReentrantLock 的使用
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 释放锁
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
(2)tryLock () 的使用
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockExample {
private final Lock lock = new ReentrantLock();
public void performTask() {
if (lock.tryLock()) { // 尝试获取锁
try {
// 执行关键操作
System.out.println("获取到锁,执行任务");
} finally {
lock.unlock();
}
} else {
// 处理无法获取锁的情况
System.out.println("未获取到锁,执行其他操作");
}
}
}
(3)ReentrantReadWriteLock 的使用
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Cache {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private String data;
public String read() {
rwLock.readLock().lock(); // 获取读锁
try {
return data;
} finally {
rwLock.readLock().unlock();
}
}
public void write(String newData) {
rwLock.writeLock().lock(); // 获取写锁
try {
data = newData;
} finally {
rwLock.writeLock().unlock();
}
}
}
(4)StampedLock 的使用
import java.util.concurrent.locks.StampedLock;
public class Point {
private double x, y;
private final StampedLock lock = new StampedLock();
public double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead(); // 尝试乐观读
double currentX = x, currentY = y;
if (!lock.validate(stamp)) { // 验证读期间是否有写操作
stamp = lock.readLock(); // 获取悲观读锁
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.hypot(currentX, currentY);
}
public void move(double deltaX, double deltaY) {
long stamp = lock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp);
}
}
}
4. 条件变量(Condition)的使用
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedQueue {
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items;
private int count, head, tail;
public BoundedQueue(int capacity) {
items = new Object[capacity];
}
public void enqueue(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 队列满时等待
items[tail] = x;
if (++tail == items.length)
tail = 0;
count++;
notEmpty.signal(); // 通知队列非空
} finally {
lock.unlock();
}
}
public Object dequeue() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 队列空时等待
Object x = items[head];
if (++head == items.length)
head = 0;
count--;
notFull.signal(); // 通知队列未满
return x;
} finally {
lock.unlock();
}
}
}
5. 公平锁与非公平锁
- 公平锁:线程按照请求锁的顺序依次获取锁,能减少线程饥饿的情况,但可能会降低系统的吞吐量。
Lock fairLock = new ReentrantLock(true); // 创建公平锁
- 非公平锁:线程获取锁的顺序是不确定的,吞吐量相对较高,不过可能导致某些线程长时间无法获取到锁。
Lock unfairLock = new ReentrantLock(); // 默认是非公平锁
6. 注意要点
- 锁的释放:必须在
finally
块中释放锁,防止出现死锁的情况。 - 锁的嵌套:可重入锁允许同一线程多次获取同一把锁,但要注意正确释放锁。
- 性能考量:在竞争激烈的场景下,
Lock
的性能通常优于synchronized
,不过在竞争不激烈的情况下,两者的性能差异并不明显。 - 使用场景:当需要灵活的锁操作(如超时、可中断、公平性)时,应优先考虑使用
Lock
;而在简单的同步场景下,使用synchronized
更为简洁。
7. 锁的选用建议
- ReentrantLock:适用于需要灵活锁操作的场景,比如支持锁的中断、超时等。
- ReentrantReadWriteLock:在读多写少的场景下能显著提升性能。
- StampedLock:在读多写少且对读性能要求极高的场景中使用,不过它的使用方式相对复杂。
通过合理运用Lock
接口及其实现类,能够更精细地控制线程同步,从而提升多线程程序的性能和可靠性。