什么是管程?
管程:指的是管理共享变量以及对共享变量操作得过程,让他们支持并发。
在管程得发展史上,先后出现过三种不同得管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在广泛再用得就是MESA模型。
java得管程模型就是基于MESA改进得,AQS也是基于这套思想设计得。
了解ReentrantLock必须先熟悉AQS,AQS提供了一些模板方法供具体的子类实现,从而根据AQS实现公平锁,非公平锁等。
首先要明白几个概念:
1.临界区:临界区是指进程中访问临界资源的一段需要互斥执行的代码。进入临界区之前需要判断能否进入,进入时需要改变标志阻止其他进程进入,进入的进程执行完成后退出时修改标志;
2.公平锁:先到达临界区的线程先获取到锁,后到达临界区的线程后获取到锁;
3.非公平锁:先到达临界区的线程未必先获取到锁。
4.可重入锁:当前获取到锁的线程,可以再次获取锁。
接下来我们就看一下ReentrantLock是如何实现这些功能的:
ReentrantLock对象中维护着一个至关重要的变量Sync,Sync是一个抽象类,有两个具体实现
FairSync(公平锁),NonfairSync(非公平锁)
看一下Sync 的类关系图
ReentrantLock是通过变量state设置锁的状态,若为0即为五哦所状态,非0就是加锁状态
非公平于公平锁的不同之处在于:非公平锁在进入临界区时就会尝试去加锁,不管队列中是否存在排队的线程;公平锁则会先判断队列中是否存在排队的线程,不存在才会去尝试加锁。
//非公平锁实现方式
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//公平锁实现方式
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//若state==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;
}
在加锁成功后,会将变量exclusiveOwnerThread设置为当前线程,如果加锁失败,会判断exclusiveOwnerThread是否为当前线程,若为当前线程,则会将state自增加1,加锁成功。这便是可重入锁的实现方式。
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;
}
如果可重入尝试也加锁失败,则会尝试将当前线程加入队列中acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
这段逻辑中有两个for循环
第一个for循环在addWaiter(Node.EXCLUSIVE)方法中,EXCLUSIVE表时独占
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;
}
}
}
}
首先会将当前线程封装成Node对象,通过cas将node放入尾节点,若加入失败则进入enq(node)逻辑;这个for循环的作用就是如果队列不存在则创建一个队列,然后入队,如果存在则通过cas再次入队,for循环保证node肯定能加入到等待队列中。这就是第一个for循环的用处。
我们再来看第二个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);
}
}
此处会判断当前节点的前一个节点是否为头节点,若是则会再次尝试加锁。加锁成功后会执行
setHead(node)方法:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
这里将头节点设置为当前节点,且把当前节点得thread设置为null,这与前面初始化同步队列时会设置一个空得头部节点compareAndSetHead(new Node())相互对应,表明有一个线程已经获取到锁了,其余线程需排在这个空节点后面。
若加锁失败则进入shouldParkAfterFailedAcquire(p,node)逻辑
/**
*pred 前一个节点
*node 当前节点
**/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//SIGNAL表示待唤醒状态
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;
}
如果前一个节点的waitStatus不是-1,则通过cas将前一个节点的状态设置成-1,waitStatus默认为0。如果返回为false,由于for循环会再次进入这里,最终这里返回为true。最终会阻塞当前线程
private final boolean parkAndCheckInterrupt() {
//阻塞当前线程
LockSupport.park(this);
return Thread.interrupted();
}
所以第二个for循环就是保证当前线程若未获取到锁则一定会进入阻塞状态。
以上就是加锁过程,获取不到锁最终阻塞。
解锁逻辑就很简单,就是将state自减1,若减至0,则将变量exclusiveOwnerThread设置为null,
同时唤醒当前节点的下一个节点,让其去竞争锁。
补充:公平锁效率高还是非公平锁效率高?
非公平锁效率是高于公平锁的,因为非公平锁会有机会减少线程上下文切换的次数,如果当前活跃的线程有机会获取到锁,则直接获取,不用让其阻塞,减少线程上下文切换此时;公平锁则只要队列中有排队线程则必须去入队,阻塞,可能会造成一些无意义的线程上下文切换。非公平锁带来的影响就是有可能造成活锁,就是在排队的线程可能很久都获取不到锁,但这种概率很低。