独占锁 ReentrantLock 原理
介绍
ReentrantLock 是 可重入 的独占锁,同时只能有一个线程能够获取该锁,其余获取该锁的线程会被 阻塞 而后放入到该锁的 AQS 阻塞队列
中。
它具备与 synchronized
雷同的根本行为和语义,但 ReentrantLock 更灵便、更弱小,减少了轮询、超时、中断等高级性能,并且还反对 偏心锁和非偏心锁。
从类图能够看到,ReentrantLock 还是应用 AQS 来实现的,并且能够依据参数来抉择其外部是一个偏心锁还是非偏心锁,默认为非偏心锁。
public ReentrantLock() {sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
Sync 类间接继承 AQS 类,它的子类 NonfairSync 和 FairSync 别离实现了获取锁的非偏心和偏心策略。
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
在 ReentrantLock 中 AQS 的 state 状态值示意线程获取锁的可重入次数,在默认状况下,state 值为 0 示意以后所没有任何线程持有。当一个线程第一次获取该锁后,会尝试应用 CAS 将 state 设置为 1,如果 CAS 胜利则以后线程获取该锁,而后记录该锁的持有者为以后线程。在该线程第二次获取该锁后,则会将 state 设置为 2,这就是可重入次数。在该线程开释锁时,会尝试应用 CAS 将 state 减 1,如果减 1 后为 0 则开释锁。
获取锁
void lock()
public void lock() {sync.lock();
}
当线程调用该办法时,如果以后锁没有被其余线程占用并且以后线程之前没获取过该锁,则以后线程获取,而后设置锁的拥有者为本人,随后设置 AQS 的 state 为 1,而后返回。
如果以后线程曾经获取过该锁,则只是简略的将 AQS 的 state 加 1,而后返回。
如果该锁曾经被其余线程所持有,则以后线程会进入 AQS 的阻塞队列中阻塞挂起。
在 ReentrantLock 中的 lock()办法委托给了 sync 类,sync 则依据 ReentrantLock 的构造函数抉择应用 NonfairSync 或 FairSync。
咱们先看看非偏心锁的实现。
final void lock() {
// CAS 设置状态值
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 调用 AQS 的 acquire 办法
acquire(1);
}
在 lock()中首先调用了 compareAndSetState 办法,因为默认 state 状态值为 0,所以第一个线程在首次调用该办法时通过 CAS 会设置为 1,随后胜利获取到该锁,而后通过 setExclusiveOwnerThread 办法将锁持有者设置为以后线程。
当有其它线程通过 lock()来获取该锁时候,会 CAS 失败进入 else 调用 acquire(1)办法并且传参数为 1,上面咱们再看一下 acquire 办法。
public final void acquire(int arg) {if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();}
之前文章讲过,AQS 没有提供 tryAcquire 办法的实现,咱们看一下 ReentrantLock 重写的 tryAcquire 办法,这里咱们还是看非偏心锁的实现。
static final class NonfairSync extends Sync {final void lock() {if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 调用该办法
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;
}
该办法首先查看以后状态值是否为 0,为 0 则阐明锁目前是闲暇状态,而后尝试 CAS 获取锁,胜利后 state 设置为 1,而后锁持有者为以后线程。
如果 state 不为 0,则阐明锁曾经被持有,如果持有者正好是以后线程则进行 state 加 1,而后返回 true,须要留神,如果 nextc < 0 则阐明锁可能溢出。
如果以后线程不是持有者则返回 false 随后退出 AQS 阻塞队列。
上面咱们看一下偏心锁的实现。
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;
}
偏心锁和非偏心锁的 tryAcquire 办法不同之处就是多了一个 hasQueuedPredecessors()办法,该办法就是实现偏心锁的外围代码。
public final boolean hasQueuedPredecessors() {
// 读取头节点
Node t = tail;
// 读取尾节点
Node h = head;
// s 是首节点 h 的后继节点
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
在该办法中,因为队列是 FIFO 的,所以须要判断队列中有没有相干线程的节点曾经在排队了。有则返回 true 示意线程须要排队,没有则返回 false 则示意线程无需排队。
首先咱们看第一个条件h != t
,
- 头节点和尾节点都为 null,示意队列都还是空的,甚至都没实现初始化,那么天然返回 fasle,无需排队。
- 头节点和尾节点不为 null 然而相等,阐明头节点和尾节点都指向一个元素,示意队列中只有一个节点,这时候也无需排队,因为队列中的第一个节点是不参加排队的,它持有着同步状态,那么第二个进来的节点就无需排队,因为它的前继节点就是头节点,所以第二个进来的节点就是第一个能失常获取同步状态的节点,第三个节点才须要排队,期待第二个节点开释同步状态。
接下来咱们看第二个条件,(s = h.next) == null
,如果 h != t && s == null
则阐明有一个元素将要作为 AQS 的第一个节点入队,则返回 true。
接下来看第三个条件,s.thread != Thread.currentThread()
,判断后继节点是否为以后线程。
🙋🏻♀️ 举例,状况一:
h != t 返回
true
,(s = h.next) == null 返回false
,s.thread != Thread.currentThread() 返回false
首先 h != t 返回true
,阐明队列中至多有两个不同节点存在;
(s = h.next) == null 返回false
,阐明头节点之后是有后继节点存在;
s.thread != Thread.currentThread()返回 false,阐明以后线程和后继节点雷同;
阐明曾经轮到以后节点去尝试获取同步状态,无需排队,返回 false。
🙋🏻♀️ 举例,状况二:
h != t 返回
true
,(s = h.next) == null 返回true
首先 h != t 返回true
,阐明队列中至多有两个不同节点存在;
(s = h.next) == null 返回true
,阐明头节点也就是哨兵节点之后没有后继节点;
返回 true,阐明须要排队。
🙋🏻♀️ 举例,状况三:
h != t 返回
true
,(s = h.next) == null 返回false
,s.thread != Thread.currentThread() 返回true
首先 h != t 返回true
,阐明队列中至多有两个不同节点存在;
(s = h.next) == null 返回false
,阐明头节点之后是有后继节点存在;
s.thread != Thread.currentThread()返回true
,阐明后继节点的线程不是以后线程,阐明后面曾经有人在排队了,还是得老老实实排队。
返回 true,阐明须要排队。
void lockInterruptibly()
public void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
// 如果以后线程被中断,则抛出异样
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取资源
if (!tryAcquire(arg))
// 调用 AQS 可被中断的办法
doAcquireInterruptibly(arg);
}
该办法和 lock()办法相似,只不过它会对中断进行响应,就是以后线程在调用该办法时,如果他其余线程调用了以后线程的 interrupt()办法,则以后线程会抛出 InterruptedException 异样。
boolean 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;
}
该办法尝试获取锁,如果以后锁没有被其余线程持有,则以后线程获取该锁并返回 true,否则返回 false。
📢 留神:该办法不会引起以后线程阻塞。
boolean tryLock(long timeout,TimeUnit unit)
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
与 tryLock 不同之处在于,设置了超时工夫,如果超时工夫到还没有获取到该锁,则返回 false。
开释锁
void unlock()
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;
// 如果不是锁持有者调用 unlock 则抛出异样
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果以后可重入次数为 0 则状况锁持有线程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
该办法首先尝试开释锁,如果 tryRelease()办法返回 true 则开释该锁,否则只是减 1,如果不是锁持有者去调用 unlock 则抛出 IllegalMonitorStateException 异样。
代码实际
/**
* @author 神秘杰克
* 公众号: Java 菜鸟程序员
* @date 2022/1/21
* @Description 应用 ReentrantLock 实现简略的线程平安 List
*/
public class ReentrantLockList {private List<String> array = new ArrayList<>();
private volatile ReentrantLock lock = new ReentrantLock();
public void add(String e) {lock.lock();
try {array.add(e);
} finally {lock.unlock();
}
}
public void remove(String e) {lock.lock();
try {array.remove(e);
} finally {lock.unlock();
}
}
public String get(int index) {lock.lock();
try {return array.get(index);
} finally {lock.unlock();
}
}
}
该类在通过操作 array 之前,通过加锁来保障同一时间只有一个线程能够操作 array,然而也只能有一个线程能够对 array 元素进行拜访。
总结
当同时有三个线程尝试获取独占锁 ReentrantLock 时,如果线程 1 获取到,则线程 2、3 都会被转换为 Node 节点随后被放入 ReentrantLock 对应的 AQS 阻塞队列中而后被挂起。
假如线程 1 在获取到锁之后,调用了锁创立的条件变量 1 进入 await 后,线程 1 就会 开释该锁
。而后线程 1 会被转换为 Node 节点插入条件变量 1 的条件队列中。
因为线程 1 开释了锁,所以阻塞队列中的线程 2,线程 3 都会有机会获取到该锁,如果应用的是偏心锁,那么线程 2 则会获取到该锁,而后从 AQS 阻塞队列中移除线程 2 对应的 Node 节点。