Java 同步器AbstractQueuedSynchroniz

2019-12-11  本文已影响0人  嗯哼嗯哼嗯哼嗯哼

Java 同步器AbstractQueuedSynchronizer

AbstractQueuedSynchronizer

AbstractQueuedSynchronizer(同步器)是用来构建锁或者其他同步组件的基础框架。同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来共自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态。

private volatile int state; //同步状态

//获取当前同步状态
protected final int getState() {
    return state;
}

//设置当前同步状态
protected final void setState(int newState) {
    state = newState;
}

//通过CAS方式设置state为update,设置成功返回true,否则返回false
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
//独占式获取锁的方法,应该被用来去实现Lock的tryLock(),如果获取成功则返回,否则把当前线程进入同步等待队列
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

//独占式获取锁,并且不响应线程中断。
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
1. 先调用tryAcquire(arg)尝试获取独占锁,如果获取成功,if表达式为false,直接返回
2. 如果tryAcquire(arg)获取失败,调用addWaiter(Node.EXCLUSIVE)构造同步独占节点,把当前线程进入同步等待队列,插入到尾部
3. 调用acquireQueued(node,arg)方法,以死循环的方式获取同步状态,如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程中断来实现


//把当前节点插入同步队列末尾
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    //快速尝试插在同步队列尾部
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果上面插入队列失败,这里会死循环插入同步队列尾部
    enq(node);
    return node;
}

再继续追踪acquireQueued()

final boolean acquireQueued(final Node node, int arg) {
    try {
        // 中断标志位为false
        boolean interrupted = false;
        // 死循环获取同步状态
        for (;;) {
        // 获取node的前一个节点
            final Node p = node.predecessor();
            // 如果p为头节点,那么尝试获取同步状态;如果pbushi头节点,则跳过尝试获取同步状态
            if (p == head && tryAcquire(arg)) {
            // 如果p为头节点,并且尝试获取同步状态成功,则设置当前node为头节点
                setHead(node);
                //断开之前的头节点p与当前头节点node的链,便于GC
                p.next = null; // help GC
                return interrupted;
            }
            // 如果获取同步状态失败,线程是否应该阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 调用LockSupport.park(this);阻塞当前线程,被唤醒后检查是否中断
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}
1. 设置中断标志位为false
2. 死循环获取同步状态,并且跳出循环的条件为,当前node的前一个节点为头节点,并且当前节点获取同步状态成功。
3. 如果获取同步状态失败,会检查线程是否应该阻塞,阻塞当前线程,被唤醒后会检查当前线程是否中断

上面的这些方法目前是把acquire()分析的比较清楚,就差tryAcquire()的具体实现了,tryAcquire是会交给具体的Lock的实现类来实现的

再看下acquireInterruptibly()方法,在获取同步状态的过程中,响应中断

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
        // 检查当前线程的中断标志位,如果中断,抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}


private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    //加入同步队列
    final Node node = addWaiter(Node.EXCLUSIVE);
    try {
        for (;;) {
        // 获取当前节点的前一个节点
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return;
            }
            // 获取同步状态失败,线程是否应该阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 调用LockSupport.park(this);阻塞当前线程,被唤醒后检查是否中断,如果当前线程中断也抛出异常
                throw new InterruptedException();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

可以发现acquireInterruptibly()相比于acquire()会多了一个对于中断标志位的响应,如果线程中断了,那么就抛出中断异常InterruptedException,其他的跟acquire() 的实现都一样。还有响应超时和中断的方法,就不再分析了

下面再分析下release()释放独占锁的实现方法

public final boolean release(int arg) {
    // tryRelease()会尝试释放当前同步状态
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
        // 调用LockSupport.unpark(s.thread),唤醒头节点的后继节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}
  1. 首先会尝试释放当前同步状态
  2. 调用LockSupport.unpark(s.thread),唤醒头节点的后继节点的线程。这里就跟上面的acquire()方法连贯起来了。在acquireQueued()里面调用的parkAndCheckInterrupt()里面的调用LockSupport.park(this)的线程会被唤醒,头节点的后继节点的线程会被唤醒,并且继续循环尝试获取同步状态。

总结:同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态

在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

同tryAcquire()一样tryRelease()也需要具体的Lock来实现具体的方法

acquireShared

下面再分析下acquireShared()获取共享锁的实现

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

1. 同步器调用tryAcquireShared()方法获取同步状态,如果获取成功返回值大于等于0
2. 获取同步状态失败,就会返回一个负数

// 共享式获取同步状态的自旋过程
private void doAcquireShared(int arg) {
    //把当前线程封装成Node 添加进同步等待队列
    final Node node = addWaiter(Node.SHARED);
    try {
        boolean interrupted = false;
        for (;;) {
        // 获取当前节点的前驱节点
            final Node p = node.predecessor();
            //如果前驱节点时头节点,并且获取同步状态成功,才会跳出自旋
            if (p == head) {
            // 尝试获取同步状态
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                // 设置当前节点为头节点,并且如果下一个节点也为获取共享锁的节点,那么接着唤醒下一个节点,使其再次尝试获取共享锁
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    return;
                }
            }
            // 当前线程是否应该阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
            // 阻塞当前线程,被唤醒后会检查当前线程的中断状态,并重置中断状态
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}
 

获取共享同步的流程

  1. 首先尝试获取同步状态,成功则返回,失败也进入自旋获取同步状态的流程
  2. 进入自旋获取同步状态的流程,先把当前线程封装成Node,添加进入等待队列
  3. 获取当前节点的前驱节点
  4. 如果前驱节点为头节点时,会尝试获取同步状态,获取成功后,设置当前节点为头节点,接着检查当前节点的后继节点是否为共享节点,如果是则唤醒后继节点
  5. 如果获取同步状态失败,检查当前线程是否应该阻塞,并且阻塞当前线程
  6. 如果线程处于中断状态,记录当前线程的中断信息

下面再分析下释放共享状态的过程

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
    // 唤醒处于等待状态的后继节点
        doReleaseShared();
        return true;
    }
    return false;
}
上一篇下一篇

猜你喜欢

热点阅读