java 并发包-AbstractQueuedSynchronizer

来源:互联网 发布:node没有成员pnext 编辑:程序博客网 时间:2024/06/06 03:11

简介

AbstractQueuedSynchronizer 就是我们常说的AQS-同步器。常用的有ReentrantLock、ReadWriteLock、CountDownLatch,内部实现都依赖AQS类,可以说AQS是实现同比必备良药。

源码分析

AQS的主要参数 state字段 表示同步的状态,需要通过传入值与state进行比较是否一致。CountDownLatch就是通过state来控制达到阻塞目的,当为0的时候,释放。

//等待队列的头节点 private transient volatile Node head; //等待队列的尾节点 private transient volatile Node tail; //同步状态 private volatile int state;

AQS内部通过FIFO队列实现排队获取资源,通过Node的waitStatus来控制节点的状态,再由状态判断资源是否应该被该线程获取,

 static final class Node {        static final Node SHARED = new Node();        static final Node EXCLUSIVE = null;       //取消状态 被打断或者超时,相当于该节点被废了        static final int CANCELLED =  1;      //等待触发状态 当unpart时,只有被unpark才能释放        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;        .....   }

获取锁资源

acquire方法是AQS提供对外占用资源的接口
1 首先调用子类重写tryAcquire方法,资源已被占用,则添加节点入队

   public final void acquire(int arg) {        if (!tryAcquire(arg) &&            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))            selfInterrupt();    }

入队主要通过自旋的方式、cas安全的将节点放至队列中

    private Node enq(final Node node) {        //自旋        for (;;) {            Node t = tail;            //尾节点为空,说明队列是空的 则赋值头部、尾部都为该节点            if (t == null) {                if (compareAndSetHead(new Node()))                    tail = head;            } else {                //将上一个节点设置为当前尾节点                node.prev = t;                //cas设置尾节点为改node 并且在设置原为节点的next指向当前node                if (compareAndSetTail(t, node)) {                    t.next = node;                    return t;                }            }        }    }    //添加等待节点    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方法,自旋直至设置成功        enq(node);        return node;    }

对当前节点进行自旋判断
1 当前节点的perv是否为头节点,如果是 这说明当前节点时排第一个的
2 再次尝试对比state是否满足当前值
1,2条件都满足,则返回
否则 往下走

  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);        }    }

这一步主要是对等待状态进行判断
1 SIGNAL 时 直接返回true 则挂起等待被唤醒
2 >0 表示取消 则删除该节点
3 修改pred值为Node.SIGNAL 那么因为是自旋的原因,下一次有可能就是执行1步骤进行挂起操作

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;    }

获取锁的过程就这样了。最后还需要说明的是,当被唤醒的时候,还要重新再次去竞争,然后循环上面的步骤,因为AQS获取锁是非公平的

释放锁

tryReleaseShared需要子类重写

 public final boolean releaseShared(int arg) {        if (tryReleaseShared(arg)) {            doReleaseShared();            return true;        }        return false;    }

1 如果head=tail 说明 队列空了,直接break
2 如果头节点的waitStatus为Node.SIGNAL 说明等待被唤醒
cas修改节点的waitStatus 成功 则 唤醒等待队列
3 继续自旋

  private void doReleaseShared() {        for (;;) {            Node h = head;            if (h != null && h != tail) {                int ws = h.waitStatus;                if (ws == Node.SIGNAL) {                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))                        continue;                               unparkSuccessor(h);                }                else if (ws == 0 &&                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))                    continue;                             }            if (h == head)                                    break;        }    }

unparkSuccessor 才是正真唤醒释放的地方
1 由于唤醒完之后,该节点将被释放,所以要保证改节点下的下一个节点时有意义的 不能为null或者被取消
2 LockSupport.unpark(s.thread); 唤醒线程

 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);    }

总结:AQS 使用cas进行安全竞争资源,如果未竞争到 则根据条件通过LockSupport.park对线程进行挂起。
如果某个线程释放资源时,则同理 通过 LockSupport.unpark唤醒线程加入竞争的行列。

各线程如果加入了node队列中,则通过FIFO公平竞争,但是毕竟是高并发的情况下,还没有加入node队列中的N个线程和node队列的第一个线程就会相互竞争资源,也就是说 获取锁是不公平的。

阅读全文
0 0
原创粉丝点击