Java中的锁
Lock接口
在Java5之前通过Synchronized关键字实现锁功能,Synchronized关键字隐式获取释放锁,不需要我们手动管理。但是对于一些需要灵活控制锁的获取与释放就需要使用Lock接口。
Lock接口核心API
方法签名 | 描述 |
---|---|
void lock() | 获取锁。若锁被其他线程持有,则当前线程阻塞直至锁释放。 |
void lockInterruptibly() throws InterruptedException | 获取锁(可中断)。等待锁的过程中可响应中断(抛出 InterruptedException )。 |
boolean tryLock() | 尝试获取锁(非阻塞)。立即返回:成功返回 true ,失败返回 false 。 |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 尝试获取锁(带超时)。若在指定时间内未获取到锁,则返回 false ,等待期间可被中断。 |
void unlock() | 释放锁。必须在 finally 块中调用,确保锁被释放。 |
Condition newCondition() | 返回一个与当前锁绑定的 Condition 对象,用于线程间的等待/通知机制(替代 wait() /notify() )。 |
使用
在finally块中释放锁是为了保证锁最终能被释放。
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
lock.unlock();
}
特性 | Synchronized | Lock 接口(如 ReentrantLock ) |
---|---|---|
语法 | Java 内置关键字,通过 monitor 隐式控制锁 | Java 5+ 的接口,需显式调用 lock() 和 unlock() |
锁管理 | 自动获取/释放锁(JVM 管理) | 手动获取/释放锁(需在 finally 中释放) |
锁类型 | 非公平锁 | 支持公平锁/非公平锁(构造函数指定) |
可中断性 | 不可中断 | 可通过 lockInterruptibly() 中断等待 |
尝试锁 | 不支持 | 支持 tryLock() 和 tryLock(timeout) |
读写锁 | 不支持 | 支持(ReentrantReadWriteLock ) |
异常处理 | 异常时自动释放锁 | 需手动释放锁,否则可能死锁 |
性能 | 低竞争场景优化后效率高 | 高竞争场景通过非阻塞特性可能更优 |
适用场景 | 简单同步(方法/代码块) | 复杂同步(中断、超时、读写分离) |
队列同步器(AQS)
AQS相当于构建锁的基础框架,主要有一个成员变量表示同步状态,FIFO队列。主要使用是子类继承同步器实现其抽象方法,用来构建各种同步器。同步器的设计是采用模版方法设计的。
同步器提供的模版方法
可重写的方法
方法签名 | 描述 |
---|---|
protected boolean tryAcquire(int arg) | 独占模式:尝试获取同步状态。若返回true ,表示获取成功;否则失败。参数:arg 为获取状态的参数(如锁的重入次数)。实现示例:CAS操作将state 从0改为1,表示获取锁。 |
protected boolean tryRelease(int arg) | 独占模式:尝试释放同步状态。若返回true ,表示释放后其他线程可获取锁。参数:arg 为释放状态的参数。实现示例:将state 减1,减至0时释放锁。 |
protected int tryAcquireShared(int arg) | 共享模式:尝试共享获取同步状态。返回值:负值表示失败;0表示成功但无剩余资源;正值表示成功且有剩余资源。实现示例:Semaphore 中获取许可时,返回剩余许可数。 |
protected boolean tryReleaseShared(int arg) | 共享模式:尝试共享释放同步状态。若返回true ,表示释放后可唤醒后续等待线程。实现示例:CountDownLatch 中计数器减至0时唤醒所有等待线程。 |
protected boolean isHeldExclusively() | 判断当前同步器是否由当前线程独占。实现示例:ReentrantLock 中判断state 是否大于0且当前线程是锁的持有者。 |
队列同步器的实现
同步器是如何完成线程同步的
-
同步队列
当线程获取同步状态失败时,会被组装成节点通过(CAS方法)加入到同步队列当中,同步队列遵循FIFO。
节点的属性与方法
同步队列
-
独占式获取同步状态获取流程
-
共享式同步状态获取与释放
-
独占式超时获取同步状态
重入锁
重入锁(ReentrantLock),可重入锁表示该锁能够支持一个线程对资源的重复加锁。
-
实现重入在内部有一个计数器,当线程重复获取锁时计数器加1,最后在释放锁时需要将计数器依次清0,即锁被获取n次,那么前(n-1)次tryRelease(int release)方法必须返回false。
-
公平与非公平锁非公平:当前线程锁释放之后随机唤醒一个等待线程,可能会造成线程饥饿。公平:当前线程锁释放之后唤醒处于同步队列队首节点,会有大量的线程切换,吞吐量低,
读写锁
ReentrantLock在同一时间只允许一个线程访问,而读写锁允许多个线程访问。在Java并发包中提供读写锁的实现是ReentrantReadWriteLock。
示例
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
public class ReadWriteLockExample {
// 共享资源
private int sharedData = 0;
private int version = 0;
// 创建读写锁
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReadLock readLock = rwLock.readLock();
private final WriteLock writeLock = rwLock.writeLock();
// 读取共享资源
public int readData() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取读锁");
// 模拟读取操作
System.out.println(Thread.currentThread().getName() + " 读取数据: " + sharedData + " (版本: " + version + ")");
return sharedData;
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放读锁");
}
}
// 写入共享资源
public void writeData(int newValue) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取写锁");
// 模拟写入操作
sharedData = newValue;
version++;
System.out.println(Thread.currentThread().getName() + " 写入数据: " + sharedData + " (新版本: " + version + ")");
} finally {
writeLock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放写锁");
}
}
public static void main(String[] args) {
ReadWriteLockExample example = new ReadWriteLockExample();
// 创建多个读线程
Thread[] readers = new Thread[3];
for (int i = 0; i < readers.length; i++) {
readers[i] = new Thread(() -> {
for (int j = 0; j < 5; j++) {
example.readData();
try {
Thread.sleep((long) (Math.random() * 500));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Reader-" + i);
}
// 创建多个写线程
Thread[] writers = new Thread[2];
for (int i = 0; i < writers.length; i++) {
final int writerId = i;
writers[i] = new Thread(() -> {
for (int j = 0; j < 3; j++) {
example.writeData(writerId * 10 + j);
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Writer-" + i);
}
// 启动所有线程
for (Thread reader : readers) {
reader.start();
}
for (Thread writer : writers) {
writer.start();
}
// 等待所有线程完成
try {
for (Thread reader : readers) {
reader.join();
}
for (Thread writer : writers) {
writer.join();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("所有线程执行完毕");
}
}
实现分析
-
读写设计
读写锁同样依赖自定义同步器来实现同步功能,读写状态对应同步器的同步状态。而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。
-
写锁获取与释放
写锁是一个支持重入的排它锁。在获取写锁之前需要判断读锁是否已被获取,保证写操作对读操作可见。写锁的释放与ReentrantLock类似。 -
读锁的获取与释放读锁是一个支持重入的共享锁,能够被多个线程同时获取。当写锁被其他线程获取时,获取读锁将进入等待
-
锁降级锁降级是指写锁降级成读锁,当线程持有写锁,执行完写操作之后先获取读锁在释放写锁。
锁降级的优势-
数据一致性:确保在写操作完成后,线程可以继续读取自己修改的数据,不会被其他线程的写操作干扰
-
减少锁竞争:避免在写操作后释放锁,然后立即重新获取读锁的开销
-
提高并发性能:在保持数据一致性的同时,允许其他线程获取读锁
-
Condition接口
condition通过调用lock的newCondition方法创建,依赖于lock对象。
示例
Condition中的方法
有界队列
Condition的实现分析
ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。
-
等待队列当调用Condition.await()会将当前线程构造成节点加入到等待队列尾部。
-
同步队列与等待队列
并发包中的Lock拥有一个同步队列和多个等待队列,由于Condition实现属于Lock内部类(Condition condition = lock.newCondition()),因此每个实例都拥有其引用。
-
等待
当前线程加入Condition的等待队列过程
-
通知