自定义AQS独占模式下的同步器来实现独享锁
自定义AQS独占模式下的同步器来实现独享锁
/**
* 自定义AQS独占模式下的同步器来实现独享锁
*/
public class Mutex implements Lock, java.io.Serializable {
/**
* 自定义AQS独占模式下的同步器
* 使用state为0表示当前锁没有被线程所持有
* 使用state为1表示当前锁已经被线程所持有
*/
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 判断锁是否被当前线程所持有
*/
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
/**
* 尝试获取锁
* 判断锁是否存在,如果锁不存在则获取锁(通过CAS控制)
*/
public boolean tryAcquire(int acquires) {
assert acquires == 1; // 值必须是1(独享锁只有一把锁嘛)
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread()); // 将当前线程设置为独占模式下拥有同步资源的线程
return true;
}
return false;
}
/**
* 尝试释放锁(要求被谁加的锁只能被谁释放)
* 判断当前拥有同步资源的线程是否为当前线程,如果不是则抛出异常,否则释放锁
* 这里有三种调用情况,锁空闲的状态下调用、锁已经被线程所持有但被并非拥有锁的线程调用、锁已经被线程所持有并被拥有锁的线程调用,只有第三种情况才能够解锁成功
*/
protected boolean tryRelease(int releases) {
assert releases == 1; // 值必须是1(独享锁只有一把锁嘛)
if (Thread.currentThread() != getExclusiveOwnerThread()) // 要求被谁加的锁只能被谁释放
throw new IllegalMonitorStateException();
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null); // 将独占模式中拥有同步资源的线程置为NULL
setState(0);
return true;
}
/**
* 提供一个Condition实例
*/
Condition newCondition() {
return new ConditionObject();
}
/**
* 判断锁是否被线程所持有
*/
final boolean isLocked() {
return getState() == 1;
}
}
/**
* 同步器
*/
private final Sync sync = new Sync();
/**
* 加锁
*/
public void lock() {
sync.acquire(1);
}
/**
* 尝试获取锁
*/
public boolean tryLock() {
return sync.tryAcquire(1);
}
/**
* 解锁
* 解锁只能调用同步器的release(),不能调用tryRelease()方法,因为tryRelease()方法只是简单的修改一下同步状态的值而已,并没有去唤醒等待队列中的线程,正常是需要唤醒等待队列中离头节点最近的同时等待状态不为CANCELLED的节点
*/
public void unlock() {
sync.release(1);
}
/**
* 返回与此Mutex绑定的Condition实例
*/
public Condition newCondition() {
return sync.newCondition();
}
/**
* 判断锁是否被线程所持有
*/
public boolean isLocked() {
return sync.isLocked();
}
/**
* 判断是否有线程在等待获取锁
*/
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
/**
* 可能抛出InterruptedException的加锁(如果线程被设置了中断标识那么直接抛出异常)
*/
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
/**
* 在指定的时间内尝试获取锁
*/
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
AQS子类(同步器)一般都是通过内部类实现,然后作为内部组件来使用。
public class Main {
static class MyRunnable implements Runnable {
private Mutex mutex = new Mutex();
@Override
public void run() {
System.out.println(String.format("%s Running", Thread.currentThread().getName()));
mutex.lock();
System.out.println(String.format("%s加锁", Thread.currentThread().getName()));
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mutex.unlock();
System.out.println(String.format("%s解锁", Thread.currentThread().getName()));
}
}
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread threadA = new Thread(runnable, "线程A");
Thread threadB = new Thread(runnable, "线程B");
Thread threadC = new Thread(runnable, "线程C");
threadA.start();
threadB.start();
threadC.start();
}
}
可以看到该独享锁是公平锁,多线程按照申请锁的顺序获取锁。
独占模式下获取同步资源的源码分析
acquire()方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
总结:当线程要获取同步资源时,可以调用acquire()或者tryAcquire()方法,acquire()方法中会调用AQS子类的tryAcquire()方法,尝试获取同步资源,如果获取同步资源成功,则直接返回,做自己的事情,否则将会执行addWaiter()方法,将当前线程封装成Node节点然后加入到等待队列当中,然后执行acquireQueued()方法,用于自旋获取同步资源,如果所有条件都满足那么最后将会执行selfInterrupt()方法。
addWaiter()方法
private Node addWaiter(Node mode) {
// 将当前线程封装成Node节点,并且指定为独占模式,独占模式Node.EXCLUSIVE为NULL,也就是说节点的nextWaiter为NULL
Node node = new Node(Thread.currentThread(), mode);
// 将节点加入到队尾当中
Node pred = tail;
if (pred != null) {
// 将当前节点的前驱指针指向尾节点
node.prev = pred;
// 通过CAS设置尾节点(如果pred指针所指向的尾节点就是当前的尾节点,也就是在这个过程当中没有其他节点插入到队尾,则将tail指针指向当前节点)
if (compareAndSetTail(pred, node)) {
// 将之前尾节点的后继指针指向当前节点
pred.next = node;
return node;
}
}
// 如果不存在尾节点,也就是队列为空,或者通过CAS设置尾节点失败(也就是在这个过程当中有其他节点插入到队尾),那么将会通过enq()方法死循环进行设置。
enq(node);
// 无论怎么样该方法最终都会返回封装了当前线程的节点。
return node;
}
总结:addWaiter()方法用于将当前线程封装成Node节点然后加入到等待队列当中,如果在这个过程中,等待队列为空或者通过CAS设置尾节点失败,那么将会通过enq()方法死循环进行设置。
enq()方法
private Node enq(final Node node) {
// 死循环
for (;;) {
Node t = tail;
// 如果尾节点为空则初始化队列,创建一个空的节点,并且将head和tail指针都指向这个节点
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 将当前节点的前驱指针指向尾节点
node.prev = t;
// 通过CAS设置尾节点(如果t指针所指向的节点就是当前的尾节点,也就是在这个过程当中没有其他节点插入到队尾,则将tail指针指向当前节点)
if (compareAndSetTail(t, node)) {
// 将之前的尾节点的后继指针指向当前节点
t.next = node;
return t;
}
}
}
}
总结:enq()方法中使用死循环初始化队列以及通过CAS设置尾节点,直到尾节点被设置成功,同时需要注意的是当队列初始化后会有一个空的头节点,该节点不包含任何的线程,然后再将当前节点加入到队列当中。
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);
// 将之前头节点的后继指针设置为null,帮助GC
p.next = null;
failed = false;
// 返回中断标识
return interrupted;
}
// 如果节点的前驱节点不是头节点,或者尝试获取同步资源失败,那么将会调用shouldParkAfterFailedAcquire()方法,判断线程能否进行阻塞,当线程能够被阻塞时,将会调用parkAndCheckInterrupt()方法阻塞线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果在执行该方法的过程中,抛出了异常(线程超时等等),则failed标识为true,那么将会执行cancelAcquire()方法,将当前节点的等待状态设置为CANCELLED,同时从等待队列中剔除。
if (failed)
cancelAcquire(node);
}
}
总结:acquireQueued()方法用于自旋获取同步资源,同时该方法的方法出口只有一个,也就是当节点的前驱节点是头节点,同时尝试获取同步资源成功,那么就会将当前节点设置为头节点,否则就会调用shouldParkAfterFailedAcquire()方法,判断线程能否进行阻塞,当线程能够被阻塞时,将会调用parkAndCheckInterrupt()方法阻塞线程,等待被唤醒,同时在执行acquireQueued()方法的过程中,如果抛出了异常,则failed标识为true,那么将会执行cancelAcquire()方法,将当前节点的等待状态设置为CANCELLED,同时从等待队列中剔除。
shouldParkAfterFailedAcquire()方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取节点的前驱节点的等待状态
int ws = pred.waitStatus;
// 如果前驱节点的等待状态为SIGNAL,那么当它释放同步资源时,将会自动唤醒离它最近的同时等待状态不为CANCELLED的后继节点,因此当前节点就可以直接阻塞了,等待被唤醒时再去尝试获取同步资源
if (ws == Node.SIGNAL)
return true;
// 如果前驱节点的等待状态为CANCELLED,那么通过循环找到前一个不为CANCELLED状态的节点,并且将当前节点的前驱指针指向该节点,将该节点的后继指针指向当前节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 通过CAS将前驱节点的等待状态设置为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
总结:shouldParkAfterFailedAcquire()方法用于判断线程能否进行阻塞,以及剔除被设置为CANCELLED状态的节点。
正常情况下,线程第一次进来shouldParkAfterFailedAcquire()方法时,会将前驱节点的等待状态设置为SIGNAL,然后再次自旋进来该方法,判断到前驱节点的等待状态为SIGNAL,直接返回,然后就进入待阻塞状态。
当该节点的前驱节点被CANCELLED时,如果前驱节点的前驱节点是头节点,那么将会唤醒当前节点,那么它会再次自旋进来该方法,判断到前驱节点的等待状态为CANCELLED,就会将当前节点的前驱指针指向前一个不为CANCELLED状态的节点,也就是头节点,然后再将头节点的后继指针指向当前节点,然后再次自旋进来该方法,判断到前驱节点的等待状态为SIGNAL,直接返回,再次进入待阻塞状态。
无论怎么样通过shouldParkAfterFailedAcquire()方法的所有节点最终都会进入待阻塞状态,也就是说等待队列中除了头节点以外的所有线程都会处于阻塞状态。
parkAndCheckInterrupt()方法
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程,blocker对象使用当前对象
LockSupport.park(this);
// 当被唤醒时返回线程的中断标识
return Thread.interrupted();
}
总结:parkAndCheckInterrupt()方法用于阻塞线程,同时当线程被唤醒时会返回线程的中断标识,尽管如果线程被设置了中断标识,但也不会影响线程继续往下执行,只不过当它成功获取到同步资源时,会调用一次selfInterrupt()方法,再次为线程设置中断标识。
selfInterrupt()方法
static void selfInterrupt() {
// 为线程设置中断标识
Thread.currentThread().interrupt();
}
总结:当获取了同步资源的线程被设置了中断标识,才会调用selfInterrupt()方法,再次为线程设置中断标识,因为在parkAndCheckInterrupt()方法中已经调用过一次Thread.interrupted()方法,避免外部又再次调用Thread.interrupted()方法导致线程的中断标识被清除。
cancelAcquire()方法
private void cancelAcquire(Node node) {
if (node == null)
return;
// 将当前节点封装的线程设置为NULL
node.thread = null;
// 通过循环获取当前节点不为CANCELLED状态的前驱节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 获取前驱节点的后继节点(如果节点的前驱节点不是CANCELLED状态,那么前驱节点的后继节点就是它自己)
Node predNext = pred.next;
// 将节点的等待状态设置为CANCELLED
node.waitStatus = Node.CANCELLED;
// 如果当前节点是尾节点,则直接通过CAS将tail指针指向当前节点不为CANCELLED状态的前驱节点,同时通过CAS将前驱节点的后继指针设置为NULL
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// 如果当前节点的前驱节点不是头节点 同时 前驱节点的等待状态为SIGNAL(如果不是SIGNAL那就设置为SIGNAL) 且 前驱节点封装的线程不为NULL
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
// 获取节点的后继节点
Node next = node.next;
// 如果后继节点的等待状态不为CANCELLED,则通过CAS将前驱节点的后继指针指向当前节点的后继节点
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next); // 这里并没有将当前节点的后继节点的前驱指针指向前驱节点(不用设置,unparkSuccessor()方法会自动跳过)
} else {
// 如果当前节点的前驱节点是头节点,则直接唤醒当前节点的后继节点,让它来剔除当前节点
unparkSuccessor(node);
}
node.next = node;
}
}
总结:如果线程在阻塞的过程当中抛出了异常,也就是直接中断acquireQueued()方法,然后执行finally语句块,由于failed标识为true,因此会执行cancelAcquire()方法,将当前节点的等待状态设置为CANCELLED,如果当前节点是尾节点,则直接通过CAS将tail指针指向当前节点不为CANCELLED状态的前驱节点,同时将该前驱节点的后继指针设置为NULL,如果当前节点的前驱节点不是头节点,则通过CAS将前驱节点的后继指针指向当前节点的后继节点,如果当前节点的前驱节点是头节点,那么唤醒当前节点的后继节点,让它来剔除当前节点。
独占模式下释放同步资源的源码分析
release()方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
// 如果队列不等于空,同时头节点的等待状态不为0,也就是头节点存在后继节点,那么调用unparkSuccessor()方法,唤醒离头节点最近的同时等待状态不为CANCELLED的后继节点。
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
总结:当获取了同步资源的线程释放同步资源时(外部线程或者头节点中的线程),将会调用release()方法,release()方法中会调用AQS子类的tryRelease()方法,尝试释放同步资源,如果释放同步资源成功,同时队列不为空以及头节点的等待状态不为0,也就是头节点存在后继节点,那么就会调用unparkSuccessor()方法,唤醒离头节点最近的(也就是头节点的后继节点)同时等待状态不为CANCELLED的后继节点,那么该节点将会通过自旋尝试获取同步资源。
unparkSuccessor()方法
private void unparkSuccessor(Node node) {
// 获取节点的等待状态
int ws = node.waitStatus;
// 如果节点的等待状态不为CANCELLED,则通过CAS将节点的等待状态设置为0(恢复成队列初始化后的状态)
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取节点的后继节点
Node s = node.next;
// 如果节点的后继指针为NULL(不能说明节点就没有后继节点)或者后继节点为CANCELLED状态,那么就从后往前寻找离当前节点最近的同时等待状态不为CANCELLED的后继节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒该后继节点中的线程
LockSupport.unpark(s.thread);
}
总结:unparkSuccessor()方法用于唤醒离节点最近的同时等待状态不为CANCELLED的后继节点,如果节点的后继指针为NULL,不能说明节点就没有后继节点,或者后继节点的等待状态为CANCELLED,则从后往前,寻找离节点最近的同时等待状态不为CANCELLED的节点,最终唤醒该节点中的线程。
独占模式下源码分析后的总结
1.当线程要获取同步资源时,可以调用acquire()或者tryAcquire()方法,acquire()方法中会调用AQS子类的tryAcquire()方法,尝试获取同步资源,如果获取同步资源成功,则直接返回,做自己的事情,否则将会执行addWaiter()方法,将当前线程封装成Node节点然后加入到等待队列当中,然后执行acquireQueued()方法,用于自旋获取同步资源,如果所有条件都满足那么最终将会执行selfInterrupt()方法。
2.addWaiter()方法用于将当前线程封装成Node节点然后加入到等待队列当中,如果在这个过程中,等待队列为空或者通过CAS设置尾节点失败(也就是当前指针所指向的尾节点并不是真正的尾节点,也就是在这个过程当中有其他节点插入到队尾),那么将会通过enq()方法死循环进行设置。
3.enq()方法中使用死循环初始化队列以及通过CAS设置尾节点,直到尾节点被设置成功,同时需要注意的是当队列初始化后会有一个空的头节点,该节点不包含任何的线程,然后再将当前节点加入到队列当中。
4.acquireQueued()方法用于自旋获取同步资源,同时该方法的方法出口只有一个,也就是当节点的前驱节点是头节点,同时尝试获取同步资源成功,那么就会将当前节点设置为头节点,否则就会调用shouldParkAfterFailedAcquire()方法,判断线程能否进行阻塞,当线程能够被阻塞时,将会调用parkAndCheckInterrupt()方法阻塞线程,等待被唤醒,同时在执行acquireQueued()方法的过程中,如果抛出了异常,则failed标识为true,那么将会执行cancelAcquire()方法,将当前节点的等待状态设置为CANCELLED,同时从等待队列中剔除。
5.shouldParkAfterFailedAcquire()方法用于判断线程能否进行阻塞,以及剔除被设置为CANCELLED状态的节点,正常情况下,线程第一次进来shouldParkAfterFailedAcquire()方法时,会将前驱节点的等待状态设置为SIGNAL,然后再次自旋进来该方法,判断到前驱节点的等待状态为SIGNAL,直接返回,然后就进入待阻塞状态,当该节点的前驱节点被CANCELLED时,如果前驱节点的前驱节点是头节点,那么将会唤醒当前节点,那么它会再次自旋进来该方法,判断到前驱节点的等待状态为CANCELLED,就会将当前节点的前驱指针指向前一个不为CANCELLED状态的节点,也就是头节点,然后再将头节点的后继指针指向当前节点,然后再次自旋进来该方法,判断到前驱节点的等待状态为SIGNAL,直接返回,再次进入待阻塞状态,无论怎么样通过shouldParkAfterFailedAcquire()方法的所有节点最终都会进入待阻塞状态,也就是说等待队列中除了头节点以外的所有线程都会处于阻塞状态。
6.parkAndCheckInterrupt()方法用于阻塞线程,同时当线程被唤醒时会返回线程的中断标识,尽管如果线程被设置了中断标识,但也不会影响线程继续往下执行,只不过当它成功获取到同步资源时,会调用一次selfInterrupt()方法,再次为线程设置中断标识,因为在parkAndCheckInterrupt()方法中已经调用过一次Thread.interrupted()方法,避免外部又再次调用Thread.interrupted()方法导致线程的中断标识被清除。
此时等待队列中除了头节点以外的所有线程都会处于阻塞状态
1.如果线程在阻塞的过程当中抛出了异常,也就是直接中断acquireQueued()方法,然后执行finally语句块,由于failed标识为true,因此会执行cancelAcquire()方法,将当前节点的等待状态设置为CANCELLED,如果当前节点是尾节点,则直接通过CAS将tail指针指向当前节点不为CANCELLED状态的前驱节点,同时将该前驱节点的后继指针设置为NULL,如果当前节点的前驱节点不是头节点,则通过CAS将前驱节点的后继指针指向当前节点的后继节点,如果当前节点的前驱节点是头节点,那么唤醒当前节点的后继节点,让它来剔除当前节点。
2.当获取了同步资源的线程释放同步资源时(外部线程或者头节点中的线程),将会调用release()方法,release()方法中会调用AQS子类的tryRelease()方法,尝试释放同步资源,如果释放同步资源成功,同时队列不为空以及头节点的等待状态不为0,也就是头节点存在后继节点,那么就会调用unparkSuccessor()方法,唤醒离头节点最近的同时等待状态不为CANCELLED的后继节点,那么该节点将会通过自旋尝试获取同步资源。
3.在调用unparkSuccessor()方法唤醒离节点最近的同时等待状态不为CANCELLED的后继节点时,如果节点的后继指针为NULL,不能说明节点就没有后继节点,或者后继节点的等待状态为CANCELLED,则从后往前,寻找离节点最近的同时等待状态不为CANCELLED的节点,最终唤醒该节点中的线程。
独占模式FAQ
为什么要用CAS设置尾节点?
如果在设置尾节点的这个过程当中,有其他节点插入到队尾,然后将tail指针指向当前节点,当前节点的前驱指针指向之前的尾节点,之前的尾节点的后继指针指向当前节点,那么中间插入的节点就会丢失。
在acquireQueued()方法中,为什么尝试获取同步资源之前,需要先判断一下当前节点的前驱节点是否是头节点?
强制要求等待队列中的节点获取同步资源的顺序必须是从队头到队尾,否则将会造成节点丢失,丢失了的节点中的线程将会永远处于阻塞状态(当同步资源被释放时,还没来得及唤醒离头节点最近同时等待状态不为CANCELLED的后继节点时,等待队列中一个排在很后的节点被唤醒,然后它将会通过自旋尝试获取同步资源,一旦它获取了同步资源,那么它将成为头节点,最终它与之前头节点之间的所有节点中的线程将会永远处于阻塞状态),同时只有当线程获取了同步资源后,它才能成为头节点(队列初始化后的头节点除外),因此头节点肯定是已经获取过同步资源的(队列初始化后的头节点除外),因此为了遵循队列中的节点获取同步资源的顺序必须是从队头到队尾,所以永远只有头节点的后继节点拥有尝试获取同步资源的权利,因此当在尝试获取同步资源之前,需要先判断一下当前节点的前驱节点是否是头节点,如果不是就不用获取了,至于头节点释放同步资源后,能否被后继节点获取到同步资源另说,因为当同步资源被释放时,被唤醒的后继节点可能还没来得获取同步资源,此时就被外部线程直接获取了,因此被唤醒的这个线程又只能再次进入阻塞状态。
为什么在unparkSuccessor()方法中,如果节点的后继指针为NULL,需要从后往前寻找离节点最近的同时等待状态不为CANCELLED的后继节点,而不从前往后进行寻找?
如果节点的后继指针为NULL,不能说明节点就没有后继节点,因为无论是在addWaiter()方法还是enq()方法将节点加入到队列,它总是先将当前节点的前驱指针指向尾节点,然后再通过CAS将tail指针指向当前节点,如果在将之前尾节点的后继指针指向当前节点之前,需要唤醒尾节点的后继节点,由于此时尾节点的后继指针仍然为NULL,因此无法通过next指针从前往后寻找,只能通过pred指针从后往前寻找。
线程在什么情况会被唤醒?
线程被唤醒只有两种情况
一种是外部线程或者头节点释放同步资源时,需要唤醒离头节点最近的同时等待状态不为CANCELLED的后继节点,那么该节点就会通过自旋尝试获取同步资源。
一种是当节点的前驱节点被CANCELLED时,如果前驱节点的前驱节点是头节点,那么将会唤醒当前节点,将当前节点的前驱指针指向前一个不为CANCELLED状态的节点,也就是头节点,然后再将头节点的后继指针指向当前节点。
等待队列中处于CANCELLED状态的节点什么时候被剔除?
cancelAcquire()和shouldParkAfterFailedAcquire()方法都可以剔除等待队列中处于CANCELLED状态的节点。
*在unparkSuccessor()中需要剔除处于CANCELLED状态的节点是为了避免同步问题,可能存在一个处于CANCELLED状态的节点未来得及被剔除,然后它又作为要唤醒的节点的后继节点。
自定义AQS共享模式下的同步器来实现共享锁
/**
* 自定义AQS共享模式下的同步器来实现共享锁
*/
public class Share {
/**
* 自定义AQS共享模式下的同步器
*/
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 存储线程获取同步资源的情况
*/
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
/**
* 初始化同步资源
*/
public Sync(int state) {
setState(state);
}
/**
* 尝试获取同步资源(需要保证是线程安全的)
*/
@Override
protected int tryAcquireShared(int arg) {
int state = getState();
int available = state - arg;
if (available >= 0 && compareAndSetState(state, available)) { // 通过CAS保证原子性
threadLocal.set(arg);
return available;
}
return -1;
}
/**
* 释放同步资源(线程释放同步资源的个数必须等于它获取同步资源的个数)
*/
@Override
protected boolean tryReleaseShared(int arg) {
if (threadLocal.get() != arg)
throw new UnsupportedOperationException();
if (compareAndSetState(getState(), getState() + arg)) { // 通过CAS保证原子性
threadLocal.set(null);
return true;
}
return false;
}
}
/**
* 初始化同步器的同步资源
*/
public Share(int permits) {
sync = new Sync(permits);
}
public Sync sync;
/**
* 获取许可
*/
public void acquire(int permits) {
sync.acquireShared(permits);
}
/**
* 尝试获取许可
*/
public boolean tryAcquire(int permits) {
return sync.tryAcquireShared(permits) >= 0;
}
/**
* 释放许可
*/
public boolean release(int permits) {
return sync.releaseShared(permits);
}
}
public class Main {
static class MyRunnable implements Runnable {
private Share share;
private int permits;
@Override
public void run() {
System.out.println(String.format("%s Running", Thread.currentThread().getName()));
share.acquire(permits);
System.out.println(String.format("%s获取了%s个许可", Thread.currentThread().getName(), permits));
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
share.release(permits);
System.out.println(String.format("%s释放了%s个许可", Thread.currentThread().getName(), permits));
}
public MyRunnable(Share share, int permits) {
this.share = share;
this.permits = permits;
}
}
public static void main(String[] args) {
Share share = new Share(10);
Thread threadA = new Thread(new MyRunnable(share,5),"线程A");
Thread threadB = new Thread(new MyRunnable(share,4),"线程B");
Thread threadC = new Thread(new MyRunnable(share,3),"线程C");
threadA.start();
threadB.start();
threadC.start();
}
}
共享模式下获取同步资源的源码分析
acquireShared()方法
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
总结:当线程要获取同步资源时,可以调用acquireShared()或tryAcquireShared()方法,acquireShared()方法中会调用AQS子类的tryAcquireShared()方法,尝试获取同步资源,如果获取同步资源成功,则直接返回,做自己的事情,否则将会调用doAcquireShared()方法。
doAcquireShared()方法
private void doAcquireShared(int arg) {
// 将当前线程封装成Node节点,然后加入到等待队列当中
// 当前节点会被指定为共享模式,共享模式Node.SHARED为一个空的节点,也就是说节点的nextWaiter不为NULL(isShared()方法返回true)
// 在调用addWaiter()方法的过程中,如果等待队列为空或者通过CAS设置尾节点失败,那么将会通过enq()方法死循环进行设置
final Node node = addWaiter(Node.SHARED);
// 失败标识
boolean failed = true;
try {
// 中断标识
boolean interrupted = false;
// 自旋
for (;;) {
// 获取节点的前驱节点
final Node p = node.predecessor();
// 如果节点的前驱节点是头节点,则尝试获取同步资源
if (p == head) {
int r = tryAcquireShared(arg);
// 如果获取同步资源成功,则调用setHeadAndPropagate()方法
if (r >= 0) {
setHeadAndPropagate(node, r);
// 将之前的头节点的后继指针设置为NULL,help gc
p.next = null;
// 如果获取了同步资源的线程被设置了中断标识,那么调用selfInterrupt()方法,再次为线程设置一个中断标识
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 如果节点的前驱节点不是头节点,或者尝试获取同步资源失败,那么将会调用shouldParkAfterFailedAcquire()方法,判断线程能否进行阻塞,当线程能够被阻塞时,将会调用parkAndCheckInterrupt()方法阻塞线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果在执行该方法的过程中,抛出了异常(线程超时等等),则failed标识为true,那么将会执行cancelAcquire()方法,将当前节点的等待状态设置为CANCELLED,同时从等待队列中剔除。
if (failed)
cancelAcquire(node);
}
}
总结:doAcquireShared()方法用于将当前线程封装成Node节点然后加入到等待队列当中,然后通过自旋获取同步资源,同时该方法的方法出口只有一个,也就是当节点的前驱节点是头节点,同时尝试获取同步资源成功,那么就会调用setHeadAndPropagate()方法,否则将会调用shouldParkAfterFailedAcquire()方法,判断线程能否进行阻塞,当线程能够被阻塞时,将会调用parkAndCheckInterrupt()方法阻塞线程,等待被唤醒,同时在执行doAcquireShared()方法的过程中,如果抛出了异常,则failed标识为true,那么将会执行cancelAcquire()方法,将当前节点的等待状态设置为CANCELLED,同时从等待队列中剔除。
setHeadAndPropagate()方法
private void setHeadAndPropagate(Node node, int propagate) {
// 获取头节点
Node h = head;
// 将当前节点设置为头节点
setHead(node);
//如果线程获取了同步资源后,仍然有剩余的可用资源(正常情况),或没有剩余的可用资源但旧的和新的头节点的等待状态为PROPAGATE时(说明实际存在可用资源),那么将会调用doReleaseShared()方法
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared()) // 排除等待队列中不为共享模式的节点
doReleaseShared();
}
}
总结:setHeadAndPropagate()方法用于将当前节点设置为头节点,同时如果当线程获取了同步资源后,仍然有剩余的可用资源(正常情况),或没有剩余的可用资源但旧的和新的头节点的等待状态为PROPAGATE时(说明实际存在可用资源),那么将会调用doReleaseShared()方法。
doReleaseShared()方法
private void doReleaseShared() {
// 使用死循环来保证CAS操作最终肯定成功
for (;;) {
// 获取头节点
Node h = head;
// 如果head指针和tail指针不是指向同一个节点,说明头节点肯定存在后继节点(使用head != tail可以避免头节点存在后继节点但是头节点的后继指针又为NULL的情况)
if (h != null && h != tail) {
// 获取头节点的等待状态,如果等待状态为SIGNAL,则通过CAS将头节点的等待状态设置为0(重置),然后唤醒离头节点最近的同时等待状态不为CANCELLED的后继节点
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
}
// 如果头节点的等待状态为0,则通过CAS将头节点的等待状态设置为PROPAGATE
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head) // 如果执行完以上步骤后,h指针指向的头节点仍然为当前的头节点,则退出循环,完成释放过程,然后做自己的事情
break;
}
}
总结:当等待队列中的线程获取了同步资源后,仍然有剩余的可用资源,或没有剩余的可用资源但旧的和新的头节点的等待状态为PROPAGATE,或者当线程释放同步资源这两种情况,都会调用doReleaseShared()方法,该方法使用死循环来保证CAS操作最终肯定成功,如果头节点存在后继节点,同时头节点的等待状态为SIGNAL时,那么将会通过CAS将头节点的等待状态设置为0(重置),然后唤醒离头节点最近的同时等待状态不为CANCELLED的后继节点,如果判断到头节点的等待状态为0,那么将会通过CAS将节点的等待状态设置为PROPAGATE,表示需要传播下去。
共享模式下释放同步资源的源码分析
releaseShared()方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
总结:当获取了同步资源的线程释放同步资源时,将会调用releaseShared()方法,releaseShared()方法中会调用AQS子类的tryReleaseShared()方法,尝试释放同步资源,如果释放同步资源成功,则会调用doReleaseShared()方法,唤醒离头节点最近的同时等待状态不为CANCELLED的后继节点。
共享模式下源码分析后的总结
1.当线程要获取同步资源时,可以调用acquireShared()或tryAcquireShared()方法,acquireShared()方法中会调用AQS子类的tryAcquireShared()方法,尝试获取同步资源,如果获取同步资源成功,则直接返回,做自己的事情,否则将会调用doAcquireShared()方法。
2.doAcquireShared()方法用于将当前线程封装成Node节点然后加入到等待队列当中,然后通过自旋获取同步资源,同时该方法的方法出口只有一个,也就是当节点的前驱节点是头节点,同时尝试获取同步资源成功,那么就会调用setHeadAndPropagate()方法,否则将会调用shouldParkAfterFailedAcquire()方法,判断线程能否进行阻塞,当线程能够被阻塞时,将会调用parkAndCheckInterrupt()方法阻塞线程,等待被唤醒,同时在执行doAcquireShared()方法的过程中,如果抛出了异常,则failed标识为true,那么将会执行cancelAcquire()方法,将当前节点的等待状态设置为CANCELLED,同时从等待队列中剔除。
3.setHeadAndPropagate()方法用于将当前节点设置为头节点,同时如果当线程获取了同步资源后,仍然有剩余的可用资源(正常情况),或没有剩余的可用资源但旧的和新的头节点的等待状态为PROPAGATE时(说明实际存在可用资源),那么将会调用doReleaseShared()方法。
4.当等待队列中的线程获取了同步资源后,仍然有剩余的可用资源,或没有剩余的可用资源但旧的和新的头节点的等待状态为PROPAGATE,或者当线程释放同步资源这两种情况,都会调用doReleaseShared()方法,该方法使用死循环来保证CAS操作最终肯定成功,如果头节点存在后继节点,同时头节点的等待状态为SIGNAL时,那么将会通过CAS将头节点的等待状态设置为0(重置),然后唤醒离头节点最近的同时等待状态不为CANCELLED的后继节点,如果判断到头节点的等待状态为0(表示并发释放同步资源),那么将会通过CAS将节点的等待状态设置为PROPAGATE,表示需要传播下去。
5.当获取了同步资源的线程释放同步资源时,将会调用releaseShared()方法,releaseShared()方法中会调用AQS子类的tryReleaseShared()方法,尝试释放同步资源,如果释放同步资源成功,则会调用doReleaseShared()方法,唤醒离头节点最近的同时等待状态不为CANCELLED的后继节点。
共享模式FAQ
有哪些场景会将节点的等待状态设置为PROPAGATE,以及它的作用是什么?
1.当线程A释放同步资源时,将当前的头节点的等待状态设置为0,然后唤醒离头节点最近的同时等待状态不为CANCELLED的后继节点,如果被唤醒的节点获取了同步资源,然后在调用setHeadAndPropagate()方法之前,线程B释放了同步资源,此时判断到头节点的等待状态为0,那么就会将头节点的等待状态设置为PROPAGATE,表示并发释放了同步资源,目前还有可用的同步资源,然后被唤醒的节点在执行setHeadAndPropagate()方法时,如果没有剩余的可用资源,但是判断到旧的头节点的等待状态为PROPAGATE,说明实际存在可用资源,那么会再次调用doReleaseShared()方法,去唤醒后继节点,尝试获取同步资源。
2.如果被唤醒的节点获取了同步资源,在将当前节点设置为头节点之后,线程A和B释放了同步资源,那么就跟场景1一样,线程B会将头节点的等待状态设置为PROPAGATE,然后被唤醒的节点在执行setHeadAndPropagate()方法时,如果没有剩余的可用资源,除了判断旧的头节点的等待状态是否为PROPAGATE以外,还需要判断新的头节点的等待状态是否为PROPAGATE。
场景一和场景二的区别是获取同步资源的线程在设置头节点之前还是头节点之后。
自定义AQS独占模式下的同步器来实现独享锁的更多相关文章
- 2.从AbstractQueuedSynchronizer(AQS)说起(1)——独占模式的锁获取与释放
首先我们从java.util.concurrent.locks包中的AbstraceQueuedSynchronizer说起,在下文中称为AQS. AQS是一个用于构建锁和同步器的框架.例如在并发包中 ...
- AQS源码深入分析之独占模式-ReentrantLock锁特性详解
本文基于JDK-8u261源码分析 相信大部分人知道AQS是因为ReentrantLock,ReentrantLock的底层是使用AQS来实现的.还有一部分人知道共享锁(Semaphore/Count ...
- AQS学习(二) AQS互斥模式与ReenterLock可重入锁原理解析
1. MyAQS介绍 在这个系列博客中,我们会参考着jdk的AbstractQueuedLongSynchronizer,从零开始自己动手实现一个AQS(MyAQS).通过模仿,自己造轮子来学习 ...
- 再谈AbstractQueuedSynchronizer:独占模式
关于AbstractQueuedSynchronizer JDK1.5之后引入了并发包java.util.concurrent,大大提高了Java程序的并发性能.关于java.util.concurr ...
- Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式
在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概 ...
- 再谈AbstractQueuedSynchronizer1:独占模式
关于AbstractQueuedSynchronizer JDK1.5之后引入了并发包java.util.concurrent,大大提高了Java程序的并发性能.关于java.util.concurr ...
- Java并发编程-再谈 AbstractQueuedSynchronizer 1 :独占模式
关于AbstractQueuedSynchronizer JDK1.5之后引入了并发包java.util.concurrent,大大提高了Java程序的并发性能.关于java.util.concurr ...
- 关于AQS——独占锁的相关方法(一)
一.序言 Lock接口是juc包下一个非常好用的锁,其方便和强大的功能让他成为synchronized的一个很好的替代品. 我们常用的一个Lock的实现类(好像也是唯一一个只实现了Lock接口的类) ...
- IIS7的集成模式下如何让自定义的HttpModule不处理静态文件(.html .css .js .jpeg等)请求
今天将开发好的ASP.NET站点部署到客户的服务器上后,发现了一个非常头疼的问题,那么就是IIS7的应用程序池是集成模式的话,ASP.NET项目中自定义的HttpModule会处理静态文件(.html ...
随机推荐
- P、NP、NPC问题详解
转载地址 https://blog.csdn.net/bcb5202/article/details/51202589 P.NP.NPC 概念 > P问题:能够在多项式时间内解决的决策问题. - ...
- LeetCode406 queue-reconstruction-by-height详解
题目详情 假设有打乱顺序的一群人站成一个队列. 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数. 编写一个算法来重建这个队列. 注意: 总人数少于 ...
- ROS 八叉树地图构建 - 给 octomap_server 增加半径滤波器!
为了在每帧点云中滤除噪声点,选择了半径滤波器,也用高斯滤波器测试过,但是没有半径效果好,这里记录下在 octomap_server 中增加半径滤波器的步骤,并在 launch 中配置滤波器参数. 一. ...
- 【AHOI 2015】 小岛 - 最短路
描述 西伯利亚北部的寒地,坐落着由 N 个小岛组成的岛屿群,我们把这些小岛依次编号为 1 到 N . 起初,岛屿之间没有任何的航线.后来随着交通的发展,逐渐出现了一些连通两座小岛的航线.例如增加一条在 ...
- 线段树(二)STEP
线段树(二) 线段树例题整理 Part 1:题面 传送门:https://www.luogu.com.cn/problem/P6492(靠之前传送门放错了,暴露了我在机房逛B站的事实-- Part 2 ...
- C++判断是回文串还是镜像串
#include <iostream> #include <string> #include <cstdio> #include <cctype> #p ...
- Promise 方法
functionB(){ this.functionA() } functionA(){ return new Promise((resolve, reject) => { this.$http ...
- 简单说说mybatis是防止SQL注入的原理
mybatis是如何防止SQL注入的 1.首先看一下下面两个sql语句的区别: <select id="selectByNameAndPassword" parameterT ...
- Jmeter 常用函数(4)- 详解 __setProperty
如果你想查看更多 Jmeter 常用函数可以在这篇文章找找哦 https://www.cnblogs.com/poloyy/p/13291704.html 前言 有看我之前写的 Jmeter 文章的童 ...
- Webstorm的常用快捷键
编辑 Ctrl + Space 基本代码完成 (任何类. 方法或变量名称) Ctrl + Shift + Enter 完整的语句 Ctrl + P (在方法调用参数) 内的参数信息 Ctrl + Q ...