并发编程在Java实际开发中,占有举足轻重的地位,在接下来的篇幅中,以java.util.concurrent包下重要的、常用的接口、实现类为切入点,逐步分析并发编程。
下文的一些插图借鉴了《Java并发编程的艺术》!当然笔者水平有限,还请指正错误!
自JDK1.5开始,Java提供了Lock接口,实现锁的机制,相对于Synchronize,Lock更为轻量级、同时增加了锁的可操作性。
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
Lock接口一共提供了五个方法。
Lock接口具有多种实现类、并且该接口作为JDK提供的标准接口,也广泛应用于一些开源框架中,掌握Lock接口及其常见实现类,对实际项目开发有重要意义。下面通过介绍其部分实现类,来解开Lock接口的神秘面纱,毕竟多线程相关的东西,总给人一种比较难的感觉。
以ReentrantLock类切入源码:
// 部分源码 。。。
// 同步器
abstract static class Sync extends AbstractQueuedSynchronizer{
}
// 非公平锁同步器
static final class NonfairSync extends Sync{
}
// 公平锁同步器
static final class FairSync extends Sync{
}
ReentrantLock提供了NonfairSync和FairSync两个静态内部类,以实现非公平锁和公平锁,这两个类的父类继承了SyncAbstractQueuedSynchronizer类,而AbstractQueuedSynchronizer是整个Lock接口实现类的基石,即队列同步器,也就是平时所说的AQS。
AQS使用了模板方法模式,使用者需要继承并重写其中的部分方法:
// 维护state
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
// 独占式获取、释放锁
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
// 共享式获取、释放锁
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
以上就是自己实现一个同步器可能需要重写的方法,分为三个部分:
state代表同步状态(The synchronization state),也可以理解为锁。获取锁:0代表初始状态,1代表获取到锁的状态,如果锁可重入,相同线程再次获取则state为2;释放锁:将state值减1,直至为0(如果已经重入),代表线程释放了锁。当然state只是一个变量,你可以自定义其值以代表锁不同的状态。
除了上述的方法之外,AQS还提供了一个静态内部类Node:
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// 节点状态
volatile int waitStatus;
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
// 线程
volatile Thread thread;
// condition的下一个节点
Node nextWaiter;
}
通过其数据结构,可以发现这是一个双端队列,prev记录前驱节点,next记录后继节点,waitStatus变量记录了节点入队后的状态:
名称 | 值 | 说明 |
---|---|---|
CANCELLED | 1 | 表示由于超时或者中断,当前节点被取消。被取消后,当前节点的状态将不再发生任何变化。 |
SIGNAL | -1 | 表示后继节点等待获取同步状态,当前节点释放同步状态、中断、取消则唤醒其后继节点,以继续获取同步状态。 |
CONDITION | -2 | 表示当前节点在等待condition |
PROPAGATE | -3 | 表示下一次获取共享同步状态将被无条件传递下去 |
0 | 初始状态 |
双端队列示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8DIwhFHM-1597051143251)(media/15953305070670/15959327478856.jpg)]
AQS持有head和tail节点,分别指向队首和队尾,队列中的各个节点分别包含了对上一个、下一个节点的引用。如:对于互斥锁,当多个线程同时获取一把锁时,只能有一个线程可以获取到该锁,其他的线程被构造成Node节点,入队并阻塞。当持有锁的线程释放锁之后,唤醒下一个节点,下一个节点可以继续获取锁。这是AQS的基本原理,跟生活中排队的场景十分相似。
ReentrantLock顾名思义即可重入锁,可重入指已经获取锁的当前线程可再次获取锁,而不必等待当前锁的释放。
ReentrantLock中的Sync抽象类继承了AbstractQueuedSynchronizer类,并提供了两个实现类FairSync、NonfairSync以实现公平锁和非公平锁,并提供了两个构造函数,默认为非公平锁,或通过指定构造函数的参数实现公平锁。
接下来通过非公平锁锁的方式分析lock、tryLock以及unlock等方法,来了解加锁、解锁以及AQS。
加锁、释放锁案例:
@Test
public void testReentrantLock() {
Lock lock = new ReentrantLock();
lock.lock();
try {
System.out.println("加锁");
} finally {
lock.unlock();
System.out.println("解锁");
}
}
注意:加锁不能写在try代码块,如果try代码块加锁未成功,则finally代码块释放锁会出现异常。
final void lock() {
// 设置同步状态
if (compareAndSetState(0, 1))
// 设置独占锁的线程为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
lock方法尝试立刻获取锁。lock方法的特性,下面会逐步介绍。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire预分析:
附,中断:
中断可以简单理解为线程的一个标识属性,表示其是否被其他线程进行了中断操作。线程被中断不代表该线程的操作被终止,而是要线程自己去判断是否被中断过,并做出后续的处理。
整体逻辑:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yPZKBNKz-1597051143252)(media/15953305070670/15959337500275.jpg)]
// 非公平锁加锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
将等待获取同步状态的线程构造为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;
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;
}
}
}
}
enq方法通过一个for“死循环”来保证节点能够被添加到队列尾部。
1、如果队列的tail(尾节点)节点为空,则队列为空,此时需要实例化head节点、tail节点(注意此时并未跳出for循环)。
2、如果tail节点不为空,则队列至少已经包含了一个节点,将新添加节点的前驱节点设置为tail节点,并将tail节点设置为新增的节点。而保证节点能够线程安全的入队则采用了CAS机制。
线程入队之后会调用acquireQueued方法,以“自旋”的方式获取同步状态。在获取到同步状态之前,这个for循环并不会一直无限循环下去,看代码:
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);
}
}
第一步:获取前驱节点,并判断前驱节点是否为head节点,即获取同步状态的节点必须是队列中的第一个节点,以保证队列先进先出的原则。
第二步:没有成功获取同步状态,则判断并更改节点的状态,清除并记录当前线程中断标志(如果有)、阻塞当前获取锁的线程。
第一步代码:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
第二步代码:
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程
LockSupport.park(this);
// 清除并返回线程中断标识
return Thread.interrupted();
}
// 这里已经到了获取同步状态最底层了
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
阻塞线程使用了JDK提供的LockSupport类的park方法。调用park方法后线程会进入阻塞状态,可以通过unpark()方法或者interrupt()方法唤醒该线程。这也是要清除当前线程中断标识的原因。park方法调用涉及到JVM的源码,超出本文的分析范围了。
park()方法调用后,线程进入阻塞状态,for循环也被阻塞,直至前驱节点释放锁将其唤醒,并继续获取同步状态。
公平锁和非公平锁的区别在于,公平锁保证先请求获取锁的线程一定能够先获得锁。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁与非公平锁的唯一不同在于多了!hasQueuedPredecessors()判断,该方法的作用是查询是否有线程等待获取的时间长于当前线程。通过这个判断就保证了线程获取同步状态的绝对时间顺序,即先请求的线程一定先获得同步状态。
而确保这一逻辑的是acquire方法,回顾一下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
所有获取同步状态的线程都要先调用tryAcquire尝试获取同步状态,从而保证了先到的线程一定能够先获取到同步状态,或者先加入到同步队列,从而保证了锁的公平性。
通过unlock方法执行解锁操作,调用release方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i6spUNSi-1597051143254)(media/15953305070670/15959338321882.jpg)]
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
1、释放同步状态。
2、唤醒后继节点。
释放同步状态:
protected final boolean tryRelease(int releases) {
// 该表达式为了解锁的重入
int c = getState() - releases;
// 释放锁的线程必须与AQS中持有同步状态的线程相同
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
需要注意一点,如果当前锁已经发生了重入行为,需逐步递减同步状态,当同步状态值为0时,表示完全释放了同步状态。
唤醒后继节点:
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 回溯,保证后继节点及其状态的合法性
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
前文提到,获取同步线程失败的线程,被构造成Node节点,加入到队列并阻塞,这里调用LockSupport.unpark(s.thread)方法,将其唤醒以继续获取同步状态。LockSupport工具类底层使用了unsafe的本地方法,涉及到JVM源码这里不做深入的分析,因为我也不会。
除了lock()方法外,Lock接口还提供了tryLock()和tryLock(long time, TimeUnit unit),定义如下:
// lock
void lock();
// tryLock
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
从方法定义上对比lock()方法,tryLock()方法、tryLock(long time, TimeUnit unit)有返回值,且后者可以指定获取锁的超时时间、会抛出InterruptedException异常,也就是说tryLock(long time, TimeUnit unit)是可以响应线程中断的。
这里只分析非公平锁模式下的tryLock(long time, TimeUnit unit)方法的实现:
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
tryAcquire方法前文已有介绍,下面看doAcquireNanos方法。
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
// 1.超时
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
方法整体逻辑与前文介绍的acquireQueued方法相似,但是多了对超时时间的处理、对线程中断的响应、取消获取同步状态等操作。
这里依然只分析非公平锁下的tryLock()方法。相对前文介绍的两种加锁方式,这种加锁就相当简单了:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
与前面提到的两种加锁方式最大的不同:当前线程获取同步状态失败时,tryLock方法直接返回false。
前文提到tryLock(long time, TimeUnit unit)方法会响应中断,除了该方法,Lock接口还提供了一种专门可响应中断获取锁的方法:lockInterruptibly()
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
// ①响应中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
相对于Lock方法,在①处的处理是直接抛出异常。其他的逻辑前文有介绍,不多赘述。