Java中的锁
本篇博客参考《Java并发编程的艺术》,主要介绍Java并发包中与锁相关的API和组件,以及其简要的使用方式和实现细节。概要如下:
- Lock接口
- 队列同步器
- 重入锁
- 读写锁
- LockSupport工具
- Condition接口
Lock接口
与synchronized相比,Lock在使用时需要显式进行加锁和释放锁,少了隐式获取与释放的便捷性,但多出了锁获取与释放的可操作性、可中断性以及超时获取等特性。
队列同步器
队列同步器AbstractQueuedSynchronizer是用来构建锁或者其他同步组件的基础框架,使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作
同步器的主要使用方式是继承,子类通过继承来管理同步状态。对同步状态的修改需要使用同步器提供的3个方法getState()、setState(int newState)和compareAndSetState(int expect,int update)来进行操作。子类被推荐定义为自定义同步组件的静态内部类。同步器支持独占式和共享式获取同步状态。
同步器是实现锁的关键,我们可以这么理解:同步器面向锁的实现者,而锁面向使用者。同步器简化了锁的实现方式,屏蔽了同步状态管理、线程排队、等待与唤醒等底层操作。
队列同步器的接口与实例
同步器的设计是基于模板方法的,使用者需要继承同步器并重写指定方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
重写同步器指定的方法时,需使用以下三个方法来访问或修改同步状态:
方法 | 描述 |
---|---|
getState() | 获取当前同步状态 |
setState(int newState) | 设置当前同步状态 |
compareAndSetState | 使用CAS设置当前状态,使其保证原子性 |
同步器可重写的方法如下:
方法 | 描述 |
---|---|
protected boolean tryAcquire(int arg) | 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 |
protected boolean tryRelease(int arg) | 独占式释放同步状态 |
protected int tryAcquireShared(int arg) | 共享式获取当前同步状态,返回值大于等于0则表示成功 |
protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 |
protected boolean isHeldExclusively() | 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所占 |
部分模板及描述如下:
方法 | 描述 |
---|---|
void acquire(int arg) | 独占式获取同步状态 |
void acquireInterruptibly(int arg) | 与acquire(int arg)相同,但响应中断 |
boolean tryAcquireNanos(int arg,long nanos) | 在acquireInterruptibly(int arg)上加上超时限制 |
void acquireShared(int arg) | 共享式地获取当前同步状态 |
void acquireSharedInterruptibly(int arg) | 和上相同,该方法响应中断 |
boolean tryAcquireSharedNanos(int arg,long nanos) | 在acquireSharedInterruptibly(int arg)的基础上增加超时限制 |
boolean release(int arg) | 独占式地释放同步状态 |
boolean releaseShared(int arg) | 共享式地释放同步状态 |
Collection<Thread> getQueuedThreads() | 获取等待在同步队列上的线程的集合 |
同步器提供的模板方法基本上分为3类:独占式获取与释放、共享式获取与释放和查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。
下面是一个独占锁的示例:
public class Mutex implements Lock {
//静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
//是否处于占用状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
//当状态为0的时候获取锁
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {//cas操作,当旧的值为0时则更新为1,成功返回true,反之false
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();
}
}
//仅需将操作代理到Sync上即可
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();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
}
独占锁Mutex是一个自定义的同步组件,它在同一时刻只允许一个线程占有锁。
队列同步器的实现分析
接下来将从实现角度分析同步器是如何完成线程同步的,主要包括:
- 同步队列
- 独占式同步状态获取与释放
- 共享式同步状态获取与释放
- 超时获取同步状态
同步队列
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时阻塞当前线程,当前同步状态释放时,会把节点中的线程唤醒,使其再次尝试获取同步状态。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点。
节点是构成同步队列的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入到该队列的尾部。
同步器包含了两个节点类型的引用,一个指向头节点,一个指向尾结点。同步器提供一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。
设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能成功获取同步状态,所以设置头节点的方法不需要使用CAS来保证。
独占式同步状态获取与释放
调用同步器的acquire(int arg)方法可以获取同步状态,并且该方法对中断不敏感,也就是线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从队列中移出。该方法如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
其主要完成同步状态获取(tryAcquire(int arg))、节点构造(Node.EXCLUSIVE)、加入同步队列(addWaiter(Node node))以及在同步队列中自旋等待(acquireQueued(Node node, int arg))的相关工作。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
下面分析一些相关工作。
节点的构造以及加入同步队列(同步器的addWaiter和enq方法):
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 快速尝试在尾部添加
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
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;
}
}
}
}
上述代码通过使用compareAndSetTail(Node expect, Node update)方法来确保节点能被安全添加。在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,只有通过CAS将节点设置为尾节点后,当前线程才能从该方法返回。
节点进入同步队列后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态则从自旋中退出。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
当前线程在“死循环”中尝试获取同步状态,而且只有前驱节点是头结点才能够尝试获取同步状态,因为头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否为头节点,同时为了维护同步队列的FIFO原则。
独占式同步状态获取流程也就是acquire(int arg)方法调用流程。
当前线程获取同步状态后通过release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒后继节点。代码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
该方法执行后会唤醒头节点后继节点线程,unparkSuccessor(Node node)方法使用LockSupport来唤醒处于等待状态的线程。
总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程将成为节点加入其中并进行自旋;移出队列的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,并唤醒后继节点。
共享式同步状态获取与释放
共享式访问资源时,其他共享式访问均被允许,而独占式访问被阻塞;独占式访问资源时,同一时刻其他访问均被阻塞。
通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态,该方法代码如下: