前言
本文介绍 Java 用 ReentrantLock 实现线程安全方式,ReentrantLock 是使用代码实现的,系统无法自动释放锁。ReentrantLock 是 jdk1.5 出现,这是与原有的 synchronized 完全不同的实现线程安全的方式。高并发的情况下,ReentrantLock是个不错的方案。
ReentrantLock 底层使用 CAS 方式来实现线程安全的,这是一种性能很高的实现线程安全的一种方式。jdk8 中 ConcurrentHashMap 底层就是大量使用的 CAS 操作,源码没有使用 synchronized 的关键字。
我们看一下 ReentrantLock 的基本使用方法:
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock(true);
new Thread(() -> {
reentrantLock.lock();
System.out.println("线程1获取到锁");
try {
Thread.sleep(3000);
System.out.println("线程1执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}).start();
new Thread(() -> {
long start = System.currentTimeMillis();
reentrantLock.lock();
long end = System.currentTimeMillis();
System.out.println("等待" + (end - start) + "毫秒后线程2获取到锁");
try {
System.out.println("线程2执行完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}).start();
}
一、AbstractQueuedSynchronizer简介
AbstractQueuedSynchronizer 是 Java 并发包的基础工具类,是实现 ReentrantLock、CountDownLatch、Semaphore、FutureTask 等的基础。后面 AbstractQueuedSynchronizer 简称 AQS。
ReentrantLock 与 AQS 的实现关系:
ReentrantLock 的创建机制:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
Sync 是 ReentrantLock 的内部抽象实现类,它初步实现了 AQS:
abstract static class Sync extends AbstractQueuedSynchronizer {
}
NonfairSync(非公平锁)和 FairSync(公平锁)是 Sync 的两个实现类。
二、AQS 基本实现原理
AQS的基本属性:
// 头结点,当前持有锁的线程
private transient volatile Node head;
// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个隐视的链表
private transient volatile Node tail;
// 这个是最重要的,不过也是最简单的,代表当前锁的状态,0代表没有被占用,大于0代表有线程持有当前锁,是因为锁可以重入嘛,每次重入都加上1
private volatile int state;
// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer
这四个属性就是实现线程安全的基本属性:当第一条线程进来执行时,把当前线程放入 head 中,把 state 的初始状态修改为 1,代表当前线程持有锁,后续等待的线程进来后放入 tail 这个链表中。第一条线程执行完成后 state 会恢复初始 0,等待队列 tail 会拿出头部节点放入 head 继续执行。exclusiveOwnerThread 用来存储当前有锁的线程,每次都会比较 currentThread 与 exclusiveOwnerThread 是否是同一条线程。
记住这个概念:head 代表即将已经获得锁的线程节点 Node,tail 代表等待队列最尾的线程节点 Node。
线程都会被包装成 Node 对象,Node 是一个链表结构:
static final class Node {
// 标识节点当前在共享模式下
static final Node SHARED = new Node();
// 标识节点当前在独占模式下
static final Node EXCLUSIVE = null;
// ======== 下面的几个int常量是给waitStatus用的 ===========
volatile int waitStatus;
// 代表此线程取消了争抢这个锁
static final int CANCELLED = 1;
// 代表当前node的后继节点对应的线程需要被唤醒
static final int SIGNAL = -1;
// 代表当前node是在条件队列中,后续有介绍
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// 处于阻塞队列的节点属性,代表前驱节点的引用
volatile Node prev;
// 处于阻塞队列的节点属性,代表后续节点的引用
volatile Node next;
// 这个就是线程本尊
volatile Thread thread;
// 处于条件队列中的节点属性,代表后续节点的引用
Node nextWaiter;
}
head 和 tail 的初始化讲一下(一直没有释放锁的情况):
- Node 的初始化 waitStatus 默认值是 0,thread 默认值是 null;
- 当第一条线程来获得锁时,直接获取到不会再向下走;
- 当第二条线程进来时,会开始初始化 head 和 tail,这时 tail 指向的其实就是 head。
- 当第三条线程进来时,会把 head 的 waitStatus 置为 -1,tail 指向这个新加入的 Node,这时候 tail 的 waitStatus 是 0,thread 是当前线程;
- 以此类推,每次进来的新线程都会把前驱节点的 waitStatus 置为 -1,tail 永远指向这个新 Node。
Node 节点的 prev、next 和 nextWaiter 这三个属性代表的含义是不同,prev 和 next 是处于阻塞队列才会有的属性,nextWaiter 是处于条件队列才会有的属性,条件队列后面再介绍。也就是说,如果一个节点的 next 属性不为空,那么可以直接判断这个节点一定不在条件队列中。这个可以直接用于判断,后面代码也会看到加入到不同的队列后会把其他不相关的属性置为 null。
下面是AQS保证线程安全的实现思路,与下面源码对比理解:
三、FairSync(公平锁)的实现原理
1、加锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 争锁
final void lock() {
acquire(1);
}
}
acquire() 这个方法直接调用父类 AQS:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
前面说到 AQS 中的 state 为 0 时,代表没有线程来抢锁;为 1 时代表抢到了锁,所以 acquire(1) 传入 1,然后调用 if 中的 tryAcquire(1) ,就是试着抢一下锁,抢到了锁就 ok 了,也不需要向下执行。
没抢到锁后继续执行 addWaiter(null)。addWaiter 这个方法就是将当前线程包装成 Node 对象并返回,使用 EXCLUSIVE 独占锁的模式。再继续调用
acquireQueued(node, 1) ,将当前线程加入到等待队列挂起等着抢到锁再退出。
下面分步来说明这几个方法:
1)tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// state == 0 此时此刻没有线程持有锁
if (c == 0) {
// 虽然此时此刻锁是可以用的,但是这是公平锁,既然是公平,就得讲究先来后到,
// 看看有没有别人在队列中等了半天了
if (!hasQueuedPredecessors() &&
// 如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,
// 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了
compareAndSetState(0, acquires)) {
// 到这里就是获取到锁了,标记一下,告诉大家,现在是我占用了锁
setExclusiveOwnerThread(current);
return true;
}
}
// 会进入这个else if分支,说明是重入了,需要操作:state=state+1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 如果到这里,说明前面的if和else if都没有返回true,说明没有获取到锁
return false;
}
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
抢锁的代码比较简单,就是先查看队列中是否有等待的线程。为什么 state 为 0 时还需要查看是否有等待队列呢,因为第一种情况就是当前线程是第一条进来抢锁的线程,等待队列为空,自己拿到锁就可以了;第二种情况是上一条线程刚好释放锁,将 state 置为 0,但是后续还有等待的线程,所以当前线程需要乖乖的去排队等候。这就是公平锁的意思,如果是非公平锁,它会和等待队列中的头部节点去争抢这个锁,后续有介绍。最后还需要一个 CAS 操作,如果两条线程同时执行到这里,通过 CAS 操作只会有一条线程可以修改 state ,代表当前线程已经拿到锁了,方法返回继续后面的操作。
2)addWaiter
private Node addWaiter(Node mode) {
// 将当前线程包装为Node
Node node = new Node(Thread.currentThread(), mode);
// 进入这个方法说明没有抢到锁,就需要判断等待队列是否有等待线程
Node pred = tail;
if (pred != null) {
// 设置自己的前驱 为当前的队尾节点
node.prev = pred;
// 用CAS把自己设置为队尾, 如果成功后,tail == node了
if (compareAndSetTail(pred, node)) {
// 进到这里说明设置成功,当前node==tail, 将自己与之前的队尾相连,
// 上面已经有 node.prev = pred
// 加上下面这句,也就实现了和之前的尾节点双向连接了
pred.next = node;
// 线程入队了,可以返回了
return node;
}
}
enq(node);
return node;
}
这个方法比较简单,就将当前线程插入到等待队列中,但是会有失败的情况,第一种 pred 为 null,说明当前可能是两条线程同时进来,另外一条拿到了锁,所以现在等待队列为空;第二种情况就是 CAS 操作更新 tail 时候失败了,也就是其他线程率先加入了队列。这两种情况都会进入自旋入队模式,看下面。
3)enq
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 这是队列为空的情况
if (t == null) { // Must initialize
// 创建一个空node对象,相当于初始化tail和head
if (compareAndSetHead(new Node()))
// 给后面用:这个时候head节点的waitStatus==0, 看new Node()构造方法就知道了
// 然后再次循环进入else中
tail = head;
} else {
// 这就是竞争的方式把当前线程添加队列中,直到添加成功
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
上面说过当前线程添加到等待队列失败会有两种情况:
- 第一种就是等待队列为空的情况,进入 if 分支,初始化一个全为空的 Node,可以看 Node 的构造函数,给 head 和 tail 都赋此值。然后才可以进入 else 中,因为 else 才有返回值。
- 第二种是上个方法中竞争入队失败,在这里继续竞争入队,直到有返回。
进入到 else 中后通过不断的循环尝试将当前线程的 Node 放入到等待队列中,返回的是前驱节点 Node,也就是入队前的 tail 节点,这就没什么好说的了。
4)acquireQueued
把当前线程加入到等待队列后,携带当前线程的 Node 进入到这个方法,这个方法才是最重要的。addWaiter 后说明当前线程已经进入阻塞队列,把线程阻塞、等待锁的相关操作都在这个方法中。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
//线程是否处于中断状态
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个,因为它的前驱是head
// 所以当前节点可以去试抢一下锁
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);
}
}
这里先说明一点线程挂起和中断的区别:线程挂起,也就是 park 是真正的将线程阻塞,挂起等待唤醒;线程中断,只是一个状态,不影响程序的执行,但是进入 wait、jion、sleep 这些方法会抛出异常。
p == head 时可以试着去抢锁,因为 head 有可能是刚刚初始化空 Node,里面没有线程所以可以试着去抢,如果抢到了返回 false 即可,这个后面再说;如果没有抢到,将会进入下面两个方法中:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前驱节点的 waitStatus == -1 ,说明前驱节点状态正常,当前线程需要挂起,直接可以返回true
if (ws == Node.SIGNAL)
return true;
// 前驱节点 waitStatus大于0 ,之前说过,大于0 说明前驱节点取消了排队。
if (ws > 0) {
// 向前找到一个没有取消排队的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前驱节点的waitStatus不等于-1和1,那也就是只可能是0,-2,-3
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
shouldParkAfterFailedAcquire 这个方法检查当前线程是否需要挂起:
- 当前驱节点的 waitStatus 为 -1 时,代表前驱节点的后续节点,也就是当前线程节点需要挂起,然后进入 parkAndCheckInterrupt 这个方法,这个方法就很简单了,挂起线程,返回当前线程是否被中断的状态。
- 当前驱节点的 waitStatus 大于 0,说明前面的节点取消了排队,那么就不能等前面的节点来唤醒自己,所以向前一直找到可以唤醒自己的前驱节点即可。
- 当前驱节点的 waitStatus 等于 0,说明是刚刚初始化的 Node,需要设置前驱节点的 waitStatus 为 -1。当下次循环进入时候,这个方法就会返回 True,然后把线程挂起,等待唤醒。
所以 acquireQueued 这个方法内的线程挂起相当于把线程挂起阻塞,并返回一个是否中断的状态,只有当线程拿到锁的时候才会退出循环。最后在回到 acquire 抢锁的这个方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire 拿到锁返回 True ,不会进入后续的方法,就是代表拿到锁了;tryAcquire 没抢到锁则进入队列阻塞的这个方法,直到有人唤醒这条线程并且拿到了锁,才会返回 True,进入 selfInterrupt 这个方法:
Thread.currentThread().interrupt();//清除中断状态
2、解锁
解锁的代码比较简单,还是分开看:
1)tryRelease
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是否完全释放锁
boolean free = false;
// 其实就是重入的问题,如果c==0,也就是说没有嵌套锁了,可以释放了,否则还不能释放掉
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
tryRelease 就是尝试去解锁,如果第一次解锁后 state 为 0,说明解锁成功;如果不为 0 ,说明是嵌套锁,主程序还需要再调用 tryRelease,直到 state 为 0,完全释放锁为止。我们可以理解开始执行释放锁的线程一定是头部节点,所以查看头部节点是否为空,是否是刚刚初始化,不是的话,开始唤醒后续的线程。
2)unparkSuccessor
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果head节点当前waitStatus<0, 将其修改为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 下面的代码就是唤醒后继节点,但是有可能后继节点取消了等待(waitStatus==1)
// 从队尾往前找,找到waitStatus<=0的所有节点中排在最前面的
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从后往前找,仔细看代码,不必担心中间有节点取消(waitStatus==1)的情况
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒线程
LockSupport.unpark(s.thread);
}
唤醒线程就是直到找到后续没有取消等待的节点为止,将后续节点的线程取消挂起的状态即可。
四、NonfairSync(非公平锁)的实现原理
看懂公平锁的原理,非公平锁就很好理解了,它只是在抢锁时候进行了一些争抢的操作:
static final class NonfairSync extends Sync {
final void lock() {
// 和公平锁相比,这里会直接先进行一次CAS,成功就返回了
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
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;
}
公平锁和非公平锁的区别就在:非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了;非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。