CLHLock是自旋锁,不支持阻塞,AQS支持。
和CLHLock一样,AQS有一个头节点引用和一个尾节点引用,每当一个线程过来请求锁,就会创建一个节点,节点和线程绑定,然后插入到尾节点。
通过ReentrantLock分析AQS的阻塞锁。顺便说一下ReentrantLock这个名字,从名字可以看出,这个类默认是支持重入的(重入是指已经获取到锁的线程,可以再次获取锁)
public synchronized void a(){
b();
}
public synchronized void b(){
}
例如上面的方法a和b,上面都加了synchronized关键字,当a方法获得锁之后,再去调用方法b(也要获取锁),如果不支持重入,那么程序此时就死了,因为锁已经被他自己占用了,所以ReentrantLock一定会支持锁的重入。
首先我们先来回顾下,ReentrantLock的一般使用方式。
public class ReentrantLockTest {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock(true);
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println(new Date() + ":我获取到锁了");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}).start();
}
}
}
ReentrantLock要获取锁就要调用lock()方法,首先我们来看下lock方法的实现。
public void lock() {
sync.lock();
}
再来看一下unLock的实现
public void unlock() {
sync.release(1);
}
所有的加锁解锁的方法都是通过一个叫做sync的对象实现。
我们来看一下ReentrantLock的内部接口[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LwKf2cdE-1587126216161)(http://oi7y6lfs5.bkt.clouddn.com/DraggedImage-6.png)]
从类结构可以看出ReentrantLock支持公平锁和非公平锁,两者的区别后面再说。
sync对象继承AbstractQueuedSynchronizer。
我们来看下sync.lock()的内部实现。
首先看公平锁的实现。
final void lock() {
acquire(1);
}
很简单就一句话,调用AQS的acquire方法。
acquire的具体实现
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
竟然是一个空实现,说明此处需要子类实现此方法,既然需要子类实现此方法,为什么不写成抽象方法呢,当我们了解了AQS的实现全貌就全明白了。
这里我们看下ReentrantLock中公平锁FairSync的tryAcquire实现。
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//首先通过变量state判断锁是否被占用,0代表未被占用
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {//hasQueuedPredecessors判断队列中是否有其他线程在等待,如果没有线程在等待,设置锁的状态为1(被占用),因为获取锁的过程是多线程同时获取的,所以需要使用CAS。
setExclusiveOwnerThread(current);//设置占用排它锁的线程是当前线程
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//当前锁已被占用,但是占用锁的是当前线程本身
//还记得我们上面说过ReentrantLock是支持重入的,下面就是重入的实现,当已经获取锁的线程每多获取一次锁,state就执行加1操作
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
当获取到锁的时候返回true,当获取不到锁的时候返回false。
我们再来看下AQS中acquire方法的实现,当获取不到锁就执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。
在了解acquireQueued方法之前,先看下addWaiter方法的实现。
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;
}
}
}
}
此时AQS队列的结构如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5rXDMnc9-1587126216168)(http://oi7y6lfs5.bkt.clouddn.com/DraggedImage-7.png)]
addWaiter保证了,节点一定会被插入到队列中。
接着看acquireQueued的实现
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())//锁获取失败,首先,第一次循环设置当前节点的前一个节点的waitStatus为SIGNAL(待通知),第二次循环执行parkAndCheckInterrupt,挂起当前线程LockSupport.park(this);
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
此时AQS队列的结构如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oADWf0oz-1587126216178)(http://oi7y6lfs5.bkt.clouddn.com/DraggedImage-8.png)]
如果再有第三个线程过来,则AQS队列结构如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4X1qeSVb-1587126216186)(http://oi7y6lfs5.bkt.clouddn.com/DraggedImage-9.png)]
acquireQueued主要做了两件事
unLock内部也是通过sync对象来实现的,具体实现如下:
public void unlock() {
sync.release(1);
}
relese方法是AQS的方法
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 boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
果不其然,是空方法,我们来看下ReentrantLock对tryRelase的重写。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//锁的重入次数减1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
此时AQS的数据结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uzVQhcOI-1587126216195)(http://oi7y6lfs5.bkt.clouddn.com/DraggedImage-10.png)]
此时仅仅能说明,第二个线程可以去获取锁,但是并不能代表它已经获取到锁,因为头结点并没有变。
当第二个线程的阻塞状态被释放后,acquireQueued方法
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;
}
此时AQS的数据结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ewCZHnUe-1587126216202)(http://oi7y6lfs5.bkt.clouddn.com/DraggedImage-11.png)]
上面介绍的是公平锁的实现,那么非公平锁呢,其实很简单,我们看下ReentrantLock的非公平lock实现。
final void lock() {
if (compareAndSetState(0, 1))//直接试图获取锁,如果获取不成功,再放入队尾
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
非公平锁无非是在获取锁的时候,先去尝试获取一次,如果获取不到再加入到队尾,等待执行。而公平锁则是,直接放到队尾,等待执行。