一、独占式锁实现及应用
按上一篇文章的介绍,创建一个写锁类,用于对资源独占式的写访问,如下。写锁类中核心是创建了继承自AQS的内部类,并重写tryAcquire和tryRelease方法;写锁类实现锁接口,并创建同步器成员变量,为简便起见主要实现了Lock接口中的lock和unlock方法。
public class TestWriteLock implements Lock {
private MySync sync = new MySync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return null;
}
private static class MySync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0, arg)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if(getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
}
将该写锁做简单测试,将其应用到并发场景下数的累加场景中,如下:
public class TestWriteLockCase {
private static long sum;
private static TestWriteLock writeLock = new TestWriteLock();
public static class MyTask implements Runnable {
@Override
public void run() {
for(int i = 0; i< 1000; i++) {
writeLock.lock();
sum = sum + 1;
writeLock.unlock();
}
}
}
public static void main(String ... args) {
List<Thread> threads = new ArrayList<>();
for(int i = 0; i < 10; i++) {
threads.add(new Thread(new MyTask(), "thread-"+i));
}
threads.stream().forEach(t -> t.start());
threads.stream().forEach(t -> {
try {
t.join();
} catch (InterruptedException e){
}
});
System.out.println(sum);
}
}
代码逻辑(sum = 10 * 1000),如果注释掉锁逻辑,则结果小于等于10000;如果加上锁逻辑,则结果等于10000。下文借这个应用示例对获取锁和释放锁的逻辑进行分析。
二、独占式获取
在示例中,写锁的lock逻辑调用了AQS中独占式获取的模板方法是acquire,方法源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
主要逻辑:
- a. tryAcquire,调用用户实现的tryAcquire方法,返回成功表示当前线程获取同步状态成功;返回失败表示获取同步状态失败
- b. addWaiter,如果当前线程获取同步状态失败,则将当前线程封装成节点添加到双线链表尾部
- c. acquireQueued,如果当前节点的前驱节点是头节点,则再次尝试获取同步状态;如果再次获取失败,则判断是否将当前节点挂起;将节点挂起并检查中断状态
- d. selfInterrupt,调用中断方法设置中断标志位为true,不发生异常,便于调用者判断是否被中断过
acquire方法首先调用我们自定义的tryAcuire方法:
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0, arg)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
该方法通过CAS操作保证在并发情况下线程安全地设置同步状态,如果获取同步状态成功,则表示当前线程获取锁成功,将当前线程设置为独占线程,并返回true;如果获取同步状态失败,则表示获取锁失败,返回false。
在获取同步状态失败时,需通过addWaiter方法将当前线程封装成节点并添加到双线链表结尾。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;
}
addWaiter方法中调用了enq方法,该方法是一个无限循环,保证并发情况下所有节点都能正确地添加双向链表结尾,且在双向链表为空的情况下创建节点,并将AQS的头尾节点指向该初始节点。enq方法描述如下:
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;
}
}
}
}
节点进入同步队列后,调用acuireQueued方法让符合条件的节点(头节点的后继节点)继续获取同步状态,将不符合条件的节点挂起,并检查节点的同步状态。当同步状态释放后,符合条件的节点(头节点的后继节点)将被唤醒,再次尝试获取同步状态。acuireQueued方法描述如下:
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);
}
}
acquireQueued方法中判断节点是否需要被挂起调用的是shouldParkAfterFailedAcquire方法,其判断逻辑为:
- 如果前置节点的状态为SIGNAL(-1),则当前节点需要被挂起
- 如果前置节点的状态为CANCEL(1),则将前置节点以及前置节点之前的所有状态为CANCEL的节点去除,行程新的同步队列,本次调用返回false,交由acquireQueued方法循环处理
- 如果前置节点的状态为其它状态(0、-3,注:-2表示节点不在同步队列中),则将前置节点状态设置为SIGNAL,本次调用返回false,交由acquireQueued方法循环处理
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
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;
}
如果需要将节点挂起,则acuireQueued方法会调用parkAndCheckInterrupt方法将节点挂起,节点挂起依赖于LockSupport.park(Object blocker)方法,节点挂起后返回条件(或关系):
- 其它线程将调用LockSupport.unpark,将该线程释放
- 其它线程中断该线程
- return for no reason
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
如果acuireQueued返回true,则表示该节点线程被中断过,调用selfInterrupt方法将中断标志位设置为true,方便调用者判断该县城是否被中断过。
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
三、独占式释放
在示例中,写锁的unlock逻辑调用了AQS中独占式获取的模板方法是release,方法源码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
主要逻辑:
- a. tryRelease,调用用户实现的tryRelease方法释放同步状态,返回成功表示释放同步状态成功
- b. unparkSuccessor,将当前头结点的状态设置为0,并唤醒后继节点
release方法首先调用用户实现的tryRelease方法,如下:
protected boolean tryRelease(int arg) {
if(getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
tryRelease方法使用同步方法获取同步状态和设置同步状态,并没有使用线程安全的CAS操作,这是因为独占模式下,只有一个线程能获取到同步状态,所以释放的时候也只有一个线程,不存在竞争。首先获取同步器的同步状态,如果同步状态为0,则不需要再释放;如果不是则设置独占线程为空,重置同步状态为0。
重置同步状态成功后,如果头节点不为空或节点状态不为0,则release方法调用unparkSuccessor方法唤醒后继节点。unparkSuccessor方法描述如下:
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;
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方法中,首先用CAS操作将头节点状态设置为0,然后寻找可用的后继节点,如果直接后继节点不可用,则从后往前找到最靠前的可用后继节点,最后唤醒后继节点。后继节点唤醒后重新进入acquireQueued的循环体中,尝试获取同步状态,如果获取成功,则该后继节点称为新头节点,并断开旧头节点的链接,方便旧头节点回收。