ReentrantLock
ReentrantLock
作为java api层面的加锁方式,其性能比synchronized更好(synchronized进行优化后性能差不太多),灵活性更强
ReentrantLock | synchronized |
---|---|
可重入 | 可重入 |
可尝试加锁 | 不能尝试加锁 |
加锁时支持打断 | 加锁时不支持打断 |
加锁时可设置超时时间 | 不能设置超时时间 |
关联多个条件队列 | 关联一个条件队列 |
需要手动释放锁 | 自动释放锁 |
公平&非公平 | 非公平 |
// 获取锁
reentrantLock.lock();
// 获取锁时可相应中断
reentrantLock.lockInterruptibly();
// 尝试获取锁 获取成功返回 true
boolean b = reentrantLock.tryLock();
// 尝试获取锁,如果超过指定时间则超时, 获取成功返回 true
boolean b1 = reentrantLock.tryLock(1, TimeUnit.SECONDS);
ReentrantLock 中有一个 Sync
抽象类继承自 AbstractQueuedSynchronizer
Sync 又有两个子类,分别是 FairSync
和 NonfairSync
公平锁的实现和非公平锁的实现
我们再看看 AQS
AQS就是java中的一个类 全名叫做 AbstractQueuedSynchronizer
内部维护了一个双向列表,列表中除头节点以外其他节点都是一个阻塞的线程,每个节点都是用它的一个内部类 Node
进行封装
Node中有下列这些字段
名称 | 类型 | 默认值 | 含义 |
---|---|---|---|
SHARED | Node | new Node() | 表示共享模式 |
EXCLUSIVE | Node | null | 表示独占模式 |
CANCELLED | int | 1 | waitStatus的状态,该状态表示此线程放弃竞争锁 |
SIGNAL | int | -1 | waitStatus的状态,表示节点在等待队列中,节点线程等待唤醒 |
CONDITION | int | -2 | waitStatus的状态., 该状态表示此节点在等待队列中,等待被唤醒 |
PROPAGATE | int | -3 | 只有共享模式下才会使用,这里先不解释 |
waitStatus | int | 0 | 去上面那几种状态 |
prev | Node | null | 该节点的前驱节点 |
next | Node | null | 该节点的后继节点 |
thread | Thread | null | 该节点表示的线程 |
AQS中维护了一个 state 状态,该状态可表示是不是有线程获取了锁,具体的含义由子类进行实现
它还维护了分别指向头部和尾部的节点
AQS中的头节点是个哨兵节点,不代表任何线程
ReentrantLock
本身只实现了 Lock
接口
ReentrantLock
有两个构造方法,通过默认的构造方法创建的ReentrantLock对象默认使用的是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock实现了 Lock 接口,所以要重写 lock() 方法
public void lock() {
sync.lock();
}
它内部调用了 sync.lock() 如果我们使用默认的构造方法创建的 ReentrantLock
那么sync就是 NonfairSync
这里我们以非公平锁 NonfairSync
来讲解
假如我们有下列代码,有一个线程 t1 和 t2 ,我们先让 t1 进行启动并获取锁
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
// 获取锁
reentrantLock.lock();
new Thread(() -> {
reentrantLock.lock();
try {
while (true) {}
}finally {
reentrantLock.unlock();
}
}, "t1").start();
Thread.sleep(1000);
new Thread(() -> {
reentrantLock.lock();
try {
while (true) {}
}finally {
reentrantLock.unlock();
}
}, "t2").start();
}
加锁流程
此时 t1 先执行 lock 方法
final void lock() {
// 使用CAS的方式尝试将state更改为1
// 如果更改成功则代表锁获取成功
// t1 进来的时候 t2 并没有启动,那么t1肯定能获取到锁
if (compareAndSetState(0, 1))
// 获取锁成功,将锁的持有者设置为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 获取锁失败走这个流程
acquire(1);
}
t2 启动,并进入 lock() 方法
final void lock() {
// 由于 t1 已经获取到锁了,也就是将 state 修改成 1 了
// 那么此时t2通过 CAS的方式再次尝试获取锁将 失败
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 获取锁失败走这个流程
acquire(1);
}
t2 进入该acquire(1) 方法
这个方法位于 AQS中,并且是final修饰的,不让子类进行重写
事实上基于AQS实现的独占锁在获取锁时一般都会调用这个方法
// 这个方法在 Sync中实现
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 这时候 t1 是持有锁的,这个state已经被t1修改成 1 了
// 不过如果此时 t1 恰巧释放了锁那么 t2 state就等于0了,现在不考虑这种情况
int c = getState();
// t2 执行到这里的时候很明显该条件不成立
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;
}
// 非公平锁中的实现
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// arg 这个 参数是在实现可重入锁时使用的,暂时忽略
public final void acquire(int arg) {
// 首先会执行 tryAcquire(1) 该方法是要子类重写的,默认抛出异常
// 其实tryAcquire(1) 就是尝试获取所,如果获取成功了则返回true
// 否则会执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
由于 t1 此时持有锁,t2再去获取时就是获取锁失败
那么会执行这句代码 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
先来看看 addWaiter
干了什么事
// 刚刚调用那里传过来的mode为 : Node.EXCLUSIVE
// 表示独占锁模式
private Node addWaiter(Node mode) {
// 将当前线程封装成一个 mode
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 由于t2是第一个获取锁失败的线程,此时队列是空的
// 该条件不成立
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
// 将t2 封装的 Node返回了出去
return node;
}
private Node enq(final Node node) {
// 注意这里是个死循环
for (;;) {
Node t = tail;
// 如果此时队列是空的那么将初始化这个队列
// 经过第一次循环后队列不为空了
if (t == null) {
if (compareAndSetHead(new Node()))
// 并把头部的节点也指向了尾部
// 然后要进行下一次循环了
tail = head;
} else {
// 第二次循环会进入到这里
// 把 当前的尾部节点 t 作为 我们传过来的那个节点的前驱节点
// 我们传过来的节点就是线程 t2 封装的那个
node.prev = t;
// 然后将 t2 封装的那个节点设置成尾节点
if (compareAndSetTail(t, node)) {
// 将之前尾节点的下一个节点执行 t2
t.next = node;
// 返回的是我们当前要插入的节点也就是t2的前驱节点
// 也就是上一个尾节点
return t;
}
}
}
}
执行完 addWaiter
之后列表是这样的
现在所有的waitStatus都等于0
接下来看看 acquireQueued
方法的执行
// node 参数是 addWaiter(Node.EXCLUSIVE), arg) 返回的
// 返回的其实就是线程t2封装的那个node
// arg 还是 1
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
// 这里是标记线程在获取锁阻塞期间是否被打断过
boolean interrupted = false;
// 这里死循环
for (;;) {
// node 就是 t2,现在位于队列中的第二个,他的前驱节点是头节点
final Node p = node.predecessor();
// 判断t2的前驱节点是不是头节点 也就是判断当前这个node是不是在队列中位于第二个节点的位置
// tryAcquire(arg) 就是尝试去获取一下锁,如果获取成功了就返回true
// 但是现在 t1 还未释放锁 所以仍然是获取锁失败的
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
// shouldParkAfterFailedAcquire() 表示获取锁失败了是否需要阻塞住,如果要阻塞住返回true
// parkAndCheckInterrupt() 就是阻塞住当前线程
// 当第一次执行到这里会返回false
// 第二次执行时会返回true 然后就会阻塞住
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// pred 就是 node 的前驱节点,也就是头节点
// node 就是 t2 封装的节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 拿到头节点的watiStatus
int ws = pred.waitStatus;
// 如果等于 -1 就代表 需要阻塞住
if (ws == Node.SIGNAL)
return true;
// 大于 0 则代表放弃竞争锁
if (ws > 0) {
do {
// 循环向前查找取消节点,把取消节点从队列中剔除
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将头节点的 waitStatus设置为 -1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 返回false 不阻塞
return false;
}
// 会将当前线程阻塞住,如果是被打断的就会清除打断标记
// 因为 LockSupport.prk() 不会清除打断标记,需要手动调用 Thread.interrupted()
// 如果不清除打断标记的话,下一次LockSupport.park() 会阻塞不了
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
执行完上列方法会 线程的waitStatus会发生改变,但是队尾的waitStatus没有
解锁流程
假如此时 t1 要释放锁了
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) {
// 因为线程一 只加了一次锁即没有重入
// 所以state等于1 releases 这时也是1 1-1=0
int c = getState() - releases;
// 判断当前当前线程是否是锁的持有者
// 如果你都不持有锁肯定不会让你释放
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 等于0就代表释放成功了
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 因为只能有一个线程持有锁,所以释放锁的时候只会有一个线程执行到这里所以不需要通过CAS的方法修改state
setState(c);
return free;
}
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;
// 如果没有 下一个节点或者下一个节点已经被取消了
// 那么判断是否有尾节点并且尾节点不是当前节点
// 如果成立则从尾部向前找直到找到waitStatus小于等于0的节点为止
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒后继节点 这里将t2唤醒了
LockSupport.unpark(s.thread);
}
再来看看将t2唤醒之后 t2获取锁的流程
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 这时候 t1已经释放锁了
// 并且t2 的前驱节点就是头节点
if (p == head && tryAcquire(arg)) {
// 获取锁成功之后会将当前t2的这个节点设置为头节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// parkAndCheckInterrupt() t2 阻塞在这个方法里
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
其实整个AQS的加锁流程还是相对简单的,只要搞懂了其中涉及到的一些概念