java中锁的深入理解(二)

上一篇讲到了synchronized实现原理,这一篇一起来学习一下Lock的底层实现原理。

java.util.concurrent包中有很多lock接口的实现类,ReentrantLock,ReadWriteLock,这在我的另一篇文章中也提到了。

这些实现类的内部都使用了队列同步器AbstractQueuedSynchronizer类作为依赖。下面我们用AQS来指代AbstractQueuedSynchronizer。

AQS定义:

public abstract class AbstractQueuedSynchronizer extends
    AbstractOwnableSynchronizer implements java.io.Serializable { 
    //等待队列的头节点
    private transient volatile Node head;
    //等待队列的尾节点
    private transient volatile Node tail;
    //同步状态
    private volatile int state;
    protected final int getState() { return state;}
    protected final void setState(int newState) { state = newState;}
    ...
}
AQS中使用int型变量state来表示同步状态(0表示空闲,1表示被占中),通过内置的队列实现线程的排队工作。同步状态state,队列头指针head,队列尾指针tail都使用了volatile进行修饰,保证了多线程之间的可见性。

内部队列的定义:

static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
        ...
    }

队列中的每个节点存储了前缀节点prev,后缀节点next,节点中的线程thread,节点的状态waitStatus,状态的定义有四种,CANCELLED取消状态,SIGNAL等待触发状态,CONDITION等待条件状态,PROPAGATE状态需要向后传播。整个队列中,只有头节点中的线程是当前执行线程。

在并发的情况中,如果state的值为0,即状态为空闲,那么多个线程同时执行CAS修改state的值,成功修改state值的线程获得该锁。未获得锁的线程将生成新的节点,使用CAS的方式向后排队。当线程排到队列后面时,并不会马上插入,而是进行一个自旋操作。

final boolean acquireQueued(final Node node, int arg) {
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }

因为在node的排队的过程,头节点中的线程(即之前在执行的线程)可能已经执行完成,所以要判断该node的前一个节点pred是否为head节点(代表正在执行线程),如果pred == head,表明当前节点是队列中第一个“有效的”节点,因此再次尝试tryAcquire获取锁,如果获取到了锁,就直接将该线程设置到头节点,否则,再进行判断:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }
如果前缀节点的状态为SIGNAL,表示这个节点已经设置了锁一释放就通知它的状态,那么我们就可以安全的将线程插在它的后面,并返回true。如果前缀节点的状态是取消状态,那么就跳过前缀节点,找到前面没有被取消的第一个节点,插在其后面。否则,前缀节点的状态要么是0,要么是状态向后发散,我们直接将当前节点的状态设置为SIGNAL。以上两种情况,都无须向队列中插入,所以均返回false。

线程每次被唤醒时,都要进行中断检测,如果发现当前线程被中断,那么抛出InterruptedException并退出循环。从无限循环的代码可以看出,并不是被唤醒的线程一定能获得锁,必须调用tryAccquire重新竞争,因为锁是非公平的,有可能被新加入的线程获得,从而导致刚被唤醒的线程再次被阻塞,这个细节充分体现了“非公平”的精髓。

private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            node.compareAndSetWaitStatus(ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

线程释放锁的过程:

如果节点的状态小于0.将节点的状态设为0。然后找到下面状态值小于0的线程,将其唤醒。

总结:

Doug Lea大神写的代码相当牛,只看源码,不看应用,很难完全理解透彻,但源码看下来总体思路还是很清晰的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值