JUC-AQS

如何利用CAS实现一个同步框架

  • state:共享标记位。利用CAS修改,达到同步管理
  • 等待队列:存储需要等待获取锁的线程

共享标记位state=0 表示资源是空闲的;state=1表示有1个线程获取到资源,如何独占模式,判断持有锁的线程是否是当前线程,若是,则state变为2,达到可重入性

 如果获取锁失败立即返回,则不需要入队

如果需要不断的尝试,业务侧可循环适用用tryLock不断重试

AQS作用

定义了(共享锁、独占锁)加锁、释放锁的骨架

AbstractQueuesSynchorizer属性解释

Queues:队列

Synchorizer: 同步阻塞

volatile int state

        用于记、判断共享资源正在被占用的情况

为什么适用int而不是boolean?

为了可重入(reentrantLock多次加锁,state需要一致递增,最后需要对称释放锁)

为了共享锁模式

volatile Node head、tail

        头尾节点:线程没有获取到锁,需要排队

Node 的属性

  • Node prev、next  双向链表
  • Thread thread    当前入队等待的线程
  • Node nextWaiter     
  • int waitStatus  等待状态   1, -1, -2, -3

  核心方法(独占锁)

以下方法是骨架,实际的实现由子类完成

tryAcquire(int arg)

tryRelease(int arg)

acquire(int arg)

      获取锁

    public final void acquire(int arg) {
        // 尝试获取锁
        if (!tryAcquire(arg) &&

            // addWaiter加入等待队列  acquireQueued自旋尝试获取锁,一直获取不到park住
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

  获取锁失败,则进入等待队列

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

if (compareAndSetTail(pred, node)) {
    pred.next = node;
    return node;
}

问题: compareAndSetTail 是原子的,但是整个if语句不是原子的,为什么不会出问题?

compareAndSetTail是通过原子的方式设置尾节点(此时包含了tail=node:将当前的node节点设置尾tail),pred.next = node;只是设置之前的tail节点的next节点,这个操作不会有其他线程干扰

问题:    为什么要使用 Node pred = tail?

因为tail节点的指针会被其他线程修改,类似写时复制,如果tail节点改变,pred的指针还是指向原来的tail节点

问题:   为什么要先尝试快速入队,而不是直接入队

应该是测试的结果,认为tryAcquire失败,大概率尾节点不为空,先尝试入队,如果成功,则少了一步完整队逻辑中的 tail为null的判断

完整的入队(循环cas直到入队成功)

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

自旋尝试获取锁(自旋时间中, FIFO中的等待队列可能执行完,这样当前节点就会成为最靠前的节点,能够获取到锁)  

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {

                // 获取当前节点的上一个节点
                final Node p = node.predecessor();

              // 当前任务之前不是head,重新循环
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }

                // 判断是否需要挂起,避免过度自旋,消耗CPU
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

JUC-AQS_第1张图片 if (p == head) 直到任务1,任务2,任务3出队执行才为true,否则一直自旋

 acquire完整的流程

  • 先tryAcquire获取锁,获取锁失败,则进入等待队列
  • 先尝试“快速入队”,失败后在使用“完整入队”。(完整入队比快速入队多判断了一个tail节点是否为null,因为tryAcquire失败,大概率tail不为null,使用快速入队能提高性能)
  • 自旋获取锁(判断当前节点的前置节点是否是head节点,并且尝试获取锁,获取失败一直重试)
  • 线程挂起(重试一定次数,线程会挂起,避免无效的消耗CPU)

release(int arg)

    public final boolean release(int arg) {

         // 尝试释放锁
        if (tryRelease(arg)) {
            Node h = head;

            // 唤醒等待的线程
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

唤醒线程unpark

    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;

              // 从后往前找第一个 waitStatus <=0的
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

加锁、入队、出队、挂起、唤醒

问题:为什么从后往前找?

enq入队方法,从tail尾入队,compareAndSetTail将当前节点设置为tail后,FIFO的倒数第二个节点 ->tail还没有创建链接(t.next = node),如果从前往后找,可能会漏掉此节点。这也是为什么 node.prev = t 在 compareAndSetTail 之前的原因

node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}

  核心方法(共享锁)

acquireShared(int arg)

    public final void acquireShared(int arg) {
        
        // 尝试获取共享锁
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }




    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        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();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

待续

你可能感兴趣的:(并发编程,java)