第五章 Java中的锁
锁的作用:用来控制多个线程访问共享资源的方式。一个锁能够防止多个线程同时访问共享资源(但有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。
5.1 Lock 接口
Lock 接口提供了与 synchronized 关键字类似的同步功能,只是在使用时需要显示地获取和释放锁。虽然 Lock 接口缺少了 synchronized 方法隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种 synchronized 关键字所不具备的同步特性。
Lock 接口的 API:
方法名 | 描述 |
---|---|
void lock() | 获取锁 |
void lockInterruptibly() throws InterruptedException | 可中断的获取锁 |
boolean tryLock() | 尝试非阻塞的获取锁 |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 超时获取锁 |
void unlock() | 释放锁 |
Condition newCondition() | 获取等待通知组件 |
5.2 队列同步器
队列同步器(AbstractQueuedSynchronizer),是用来构建锁或者其他同步组件的基础框架,通过内置 FIFO 队列来完成资源获取线程的排队工作。
同步器是实现锁的关键: – 锁面向使用者,定义了使用者与锁交互的接口,隐藏了实现细节; – 同步器面向锁的实现者,简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。
5.2.1 同步器接口
队列同步器提供了三个抽象方法供实现类实现:
- getState():获取当前同步状态
- setState(int newState):设置当前同步状态
- compareAndSetState(int expect,int update):使用CAS设置当前状态
同时,同步器提供了多个可重写的方法:
- tryAcquire(int arg):独占式获取同步状态
- tryRelease(int arg):独占式释放同步状态
- tryAcquireShared(int arg):共享式获取同步状态
- tryReleaseShared(int arg):共享式释放同步状态
- isHeldExclusively():当前同步器是否被线程独占
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 使用 Mutex 时并不会直接与内部同步器的实现打交道,而是调用 Mutex 提供的方法.
* 如 获取锁的lock() 方法,只需要在方法实现中调用同步器的 acquire(int arg) 即可。
* 当前线程后去同步状态失败后,会被加入到同步队列中等待
*/
public class Mutex implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 当状态为0时获取锁
@Override
protected boolean tryAcquire(int arg) {
// 通过CAS设置state
if(compareAndSetState(0,1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放锁,将状态设置为0
@Override
protected boolean tryRelease(int arg) {
if(getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 返回一个 Condition ,每个Condition 都包含了一个 condition 队列
Condition newCondition() {
return new ConditionObject();
}
}
private final Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
5.2.2 队列同步器的实现分析
1. 同步队列
同步器内部使用一个同步队列(FIFO)来完成同步状态的管理,当线程获取同步状态失败后,会将当前线程及其等待状态等信息构造成为一个节点(Node)并加入同步队列,同时阻塞当前线程。
当同步状态释放时,会将首节点唤醒,再次尝试获取同步状态。
节点(Node)的属性类型与名称描述
属性类型与名称 | 描述 |
---|---|
int waitStatus | 等待状态 |
Node prev | 前驱节点 |
Node next | 后继节点 |
Node nextWaiter | 等待队列中的后继节点 |
Thread | 获取同步状态的线程 |
类似于双向链表
2. 独占式同步状态获取与释放
调用同步器的 acquire(int arg) 方法,可以获取同步状态,JDK 中acquire的实现如下:
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
其主要逻辑为:1. tryAcquire 尝试获取同步状态,如果失败的话则构造同步节点,并通过addWirter方法将该节点加入到同步队列的尾部;2. 调用 acquireQueued 方法,使该节点以死循环的方式获取同步状态,如果获取不到则阻塞节点中的线程。
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
3. 共享式同步状态获取与释放
共享式与独占式最主要的区别在于,同一时刻能否有多个线程同时获取到同步状态。
主要方法有:acquireShared(int arg)、tryAcquireShared(int arg)、releaseShared(int arg)
4. 独占式超时获取同步状态
同步器的 doAcquireNanos(int arg, long nanosTimeout) 方法可以超时获取同步状态,即在指定的时间段内获取同步状态,成功返回true。
5.3 重入锁 ReentrantLock
重入锁,表示该锁能够支持一个线程对资源的重复加锁。即 ReentrantLock 在调用 lock() 方法时已经获取到锁的线程,再次调用 lock() 方法依旧能够获取锁而不被阻塞。
此外,重入锁还支持获取锁时的公平性与非公平性选择。
synchronized 关键字隐式的支持重入
1. 实现重进入
重进入,指任意线程在获取锁之后能够再次获取该锁而不被阻塞,实现该特性需要解决两个问题:
- 线程再次获取锁:即锁需要识别获取的锁是否为当前占据锁的线程。
- 锁的最终释放:通过计数自增的方式表示线程重复n次加锁,锁释放也是同理。
2. 公平与非公平获取锁的区别
如果一个锁是公平的,那么锁的获取顺序就应该符合FIFO原则。
一般情况下,非公平锁的效率是要高于公平锁的。但是非公平锁可能使线程“饥饿”,即先来的线程因优先级低一直处于等待状态。
5.4 读写锁
上面的锁基本都是排他锁,在同一时刻只允许一个线程进行访问。而读写锁能在同一时刻允许多个读线程访问,但在写线程访问时,所有读、写线程均被阻塞。
5.4.1 ReentrantReadWriteLock 的特性与API
特性 | 说明 |
---|---|
公平性选择 | 支持公平与非公平的锁获取方式 |
重进入 | 支持重进入,即写锁能支持一个线程多次获取,读锁也是 |
锁降级 | 写锁能够降级为读锁 |
方法名称 | 描述 |
---|---|
int getReadLockCount() | 返回当前读锁被获取的次数 |
int getReadHoldCount() | 返回当前线程获取读锁的次数 |
boolean isWriteLocked() | 判断写锁是否被获取 |
int getWriteHoldCount() | 返回当前写锁被获取的次数 |
5.4.2 读写锁的实现分析
1. 读写状态的设计
读写锁同样依赖于自定义同步器来实现同步功能,但是读写锁的同步器需要维护多个读线程和一个写线程。
2. 写锁的获取与释放
写锁是一个支持重入的排他锁,只能被一个线程获取及重入。
如果当前线程在获取写锁时,读锁已经被获取(state!=0)或者该线程不是获取写锁的线程,则进入等待状态。
写锁的释放与 ReentrantLock 类似,都是维护一个 写状态属性,为0时表示写锁被释放,同时写锁的修改对其后的读锁可见。
3. 读锁的获取与释放
读锁是一个支持重入的共享锁,能被多个线程同时获取。
如果当前没有其他写线程,则读锁总会被成功获取。
读锁的每次释放均减少读状态。
4. 锁降级
写锁降级为读锁,指当前拥有写锁的线程,再获取读锁,随后释放之前拥有的写锁的过程。
ReentrantReadWriteLock 不支持锁升级。
5.5 LockSupport 工具
当需要阻塞或唤醒一个线程的时候,会使用 LockSupport 工具类来完成相应的工作。
方法名称 | 描述 |
---|---|
void park() | 阻塞当前线程 |
void parkNanos(long nanos) | 阻塞当前线程nanos纳秒 |
void parkUntil(long deadline) | 阻塞当前线程知道deadline |
void unpark(Thread thread) | 唤醒处于阻塞状态的线程 |
5.6 Condition 接口
Object 对象拥有一组监视器方法,包含:wait()、wait(long timeout)、notify()及notifyAll() ,这些方法与 synchronized 关键字配合,实现 等待/通知模式。
Condition 也提供了类似的方法,与Lock 配合实现 等待/通知 模式。