用ReetrantLock来分析java并发中不可错过的类AbstractQueuedSynchronizer

来源:互联网 发布:淘宝旅行机票网 编辑:程序博客网 时间:2024/05/19 22:57
     AQS类是几乎所有java.util.concurrent的抽象父类,整个AQS类的实现方式和设计思想非常值得学习和了解.也是中高级JAVA面试必备知识之一.首先通过比较熟悉的ReentrantLock类作为入口学习AbstractQueuedSynchronizer(AQS)类,同时在学习过程中尽可能的通过不同代码块进行记录疑问和疑问解答:
     AQS锁通过将等待线程放入链表的方式来实现锁的阻塞和唤醒,与synchronize关键字来区别开.不会出现synchronized关键字导致的死锁
     ReentrantLock类中存在2中锁方式----公平锁,非公平锁
     公平锁和非公平锁的底层实现机制基本上相同,只在lock时做的判断不同,以下是非公平锁入口,比公平锁多了一层if判断:

final void lock() {    if (compareAndSetState(0, 1))//判断是否锁有线程占用,如果没有就直接上锁,然后将占用线程设为当前线程        setExclusiveOwnerThread(Thread.currentThread());    else        acquire(1);//目前锁已经被占用,进行与公平锁同样的锁请求}
    acquire具体实现如下:
public final void acquire(int arg) {    if (!tryAcquire(arg)//公平锁和非公平锁实现机制不同,如果请求成功就不进行中断        &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))        selfInterrupt();//中断本线程}
以下先通过比较简单的公平锁进行解析:
protected final boolean tryAcquire(int acquires) {    final Thread current = Thread.currentThread();    int c = getState();//AQS中的states变量标明了目前是否有线程占用当前锁,如果有重用会累加    if (c == 0) {        if (!hasQueuedPredecessors()//判断是否在有比当前线程等待时间更长的线程 &&            compareAndSetState(0, acquires))//通过乐观锁进行并发处理,避免多线程顺序不同导致的锁失效         {            setExclusiveOwnerThread(current);            return true;        }    }    else if (current == getExclusiveOwnerThread()) {//锁重入机制实现,由于锁重入都是同一线程,所以下面实现没有对多线程进行处理        int nextc = c + acquires;//累加state值        if (nextc < 0)            throw new Error("Maximum lock count exceeded");        setState(nextc);        return true;    }    return false;}
由于使用短路判断,所以addWaiter方法是将获取锁失败的线程加入到AQS的线程等待链表中:
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) {//空链表初始化,初始化完成以后再挂上等待锁的节点(注意:头节点是个空节点)            if (compareAndSetHead(new Node()))                tail = head;        } else {//写入失败重新写入            node.prev = t;            if (compareAndSetTail(t, node)) {                t.next = node;                return t;            }        }    }}

在解释acquireQueued之前,我们需要先看下AQS中队列的内存结构,我们知道,队列由Node类型的节点组成,其中至少有两个变量,一个封装线程,一个封装节点类型。
而实际上,它的内存结构是这样的(第一次节点插入时,第一个节点是一个空节点,代表有一个线程已经获取锁,事实上,队列的第一个节点就是代表持有锁的节点 <--在没有吃透源码的时候这段话我不太理解,因为通过阅读源码,发现如果当前锁没有被占用,那在tryAcquire方法中就会直接使当前线程占用锁,并不会使用addWaiter方法加入等待链表,具体解答在下面代码注释中)
(转自:http://www.importnew.com/22102.html):
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);    }}
waiter双向链表除了记录前后的节点和线程外还对本线程需要进行的操作进行了记录,
这块代码有几点需要说明:
1. Node节点中,除了存储当前线程,节点类型,队列中前后元素的变量,还有一个叫waitStatus的变量,改变量用于描述节点的状态,为什么需要这个状态呢?
原因是:AQS的队列中,在有并发时,肯定会存取一定数量的节点,每个节点 代表了一个线程的状态,有的线程可能可能“等不及”获取锁了,需要放弃竞争,退出队列,有点线程在等待一些条件满足,满足后才恢复执行(这里的描述很像某个J.U.C包下的工具类,ReentrankLock的Condition,事实上,Condition同样也是AQS的子类)等等,总之,各个线程有各个线程的状态,但总需要一个变量买描述它,这个变量就叫waitStatus,它有四种状态:
Image [12]
分别表示:
1. 节点取消
2. 节点等待触发
3. 节点等待条件
4. 节点状态需要向后传播。(转自:http://www.importnew.com/22102.html)
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.         */        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//将父节点的状态设为正在等待,并返回false重新判断父节点是否是signal状态,再进行挂起    }    return false;}
private final boolean parkAndCheckInterrupt() {    LockSupport.park(this);//通过unsafe类里面的线程挂起操作挂起线程    return Thread.interrupted();//返回线程挂起状态}

至此,以上部分已经将公平锁的上锁流程分析完毕,下面会分析unlock方法
这个是ReentrantLock中的unlock方法,他直接调用了内部类的release方法,实际上这个方法是使用的AQS的release方法下面开始解析release方法
public void unlock() {    sync.release(1);}
这里会发现先使用了tryRelease方法,此方法在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 final boolean tryRelease(int releases) {    int c = getState() - releases;//首先将占用锁的线程重入次数减掉    if (Thread.currentThread() != getExclusiveOwnerThread())//如果释放锁的线程没有占用锁就报错        throw new IllegalMonitorStateException();    boolean free = false;    if (c == 0) {//如果锁被重入的次数归零了,说明所有线程重入操作完成,可以释放锁        free = true;        setExclusiveOwnerThread(null);    }    setState(c);    return free;}
至此,锁已经将占用线程清除,接下来因为链表是FIFO规则的,所以会去先唤醒头部的线程.
private void unparkSuccessor(Node node) {    int ws = node.waitStatus;    if (ws < 0)//此处又用到了节点中线程的状态进行判断,如果<0将等待状态归零        compareAndSetWaitStatus(node, ws, 0);    Node s = node.next;//前面讲过,除非第一次获取锁的线程,其他情况下运行中的线程都是在AQS链表的头部    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);//唤醒状态正确,还在等待的线程}

至此,AQS的实现类ReetrantLock公平锁的锁和解锁已经分析完毕.


参考文章:http://www.importnew.com/22102.html

0 0