java-AbstractQueuedSynchronizer(AQS)

java-AbstractQueuedSynchronizer(AQS)

学习java的并发包(JUC, java.util.concurrent),自然就回想起几个著名的类: ReentrantLock、Semaphore、CountDownLatch等。而在这些类都是基于AQS(AbstractQueuedSynchronizer)实现的,一个抽象模板类。 了解和学习AQS, 可以更好的理解和掌握JUC。

AQS支持共享和非共享两种模式,非共享对应的有ReentrantLock, 而共享对应的有CountDownLatch,semaphore等。

AQS的数据结构其实比较简单, 大致这个样子。

在这里插入图片描述

  1. head 和 tail 类型都是AQS的内部类-> node类型,
  2. state是一个int类型.也就是互斥/共享资源,state>0 资源被占用, state==0 资源被释放。

两种模式

独占模式:

  • ReentrantLock:
  • state为0代表资源未被占有,可以获取锁并设置为1,此时其他线程访问为1,代表资源已经被锁上(占有),所以线程自身进入lock(AQS)的双向队列中,等待资源释放。

共享模式:

  • CountDownLatch:
  • 一个线程(一般是主线程) latch = new CountDownLatch(num); 即state设为num
  • latch.await(); 将线程自身加入latch(AQS)的双向队列,等待资源释放
  • latch.countDown(); 将state-1,一旦state为0说明资源被释放。(一般是其他线程执行完成),之前await的线程可以继续执行。

源码分析

talk is cheap show me the code

    Semaphore semaphore = new Semaphore(5);
    semaphore.acquire();
    //doSometing
    semaphore.release();

最近用到了semaphore用来限流。 也就是dosometing的代码非常消耗内存,如果并发高很容易出现oom。这个时候就可以根据分配的Xmx和dosometing的计算最大并发个数,进行限制。

那么如何实现的并发控制,点进源码see see。

//semaphore 类的定义。
public class Semaphore implements java.io.Serializable {
    private final Sync sync;
    abstract static class Sync extends AbstractQueuedSynchronizer {...}
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    public void release() {
        sync.releaseShared(1);
    }
    // somemethods 几乎都和sync 内部类对象有关,而Sync实现的就是AQS.

acquire方法

那么先来看看acquire方法。实际执行的是sync的acquireSharedInterruptibly(1)

该方法没有被重写,在AbstractQueuedSynchronizer类中

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())   // 线程是否中断
            throw new InterruptedException();
        // 尝试获取资源 小于0 获取失败
        if (tryAcquireShared(arg) < 0)
            // 加到阻塞队列中,会再次尝试是否能获取资源。
            doAcquireSharedInterruptibly(arg);
    }

也就是尝试获取资源,发现此时资源已经被用完,所以自身线程加入aqs队列,将自己阻塞起来。

tryAcquireShared方法:

该方法重写了,在Semaphore类中。

public boolean tryAcquire() {return sync.nonfairTryAcquireShared(1) >= 0;}

final int nonfairTryAcquireShared(int acquires) {
  for (;;) {
    int available = getState();
    int remaining = available - acquires;
    // 没有资源, 获取资源失败/成功。返回剩余资源数量。没有则 < 0.
    // compareAndSetState cas操作。 尝试将资源 state = available - acquires
    if (remaining < 0 || compareAndSetState(available, remaining))
        return remaining;
  }
}

可以看到,如果有资源,则资源减一,返回的数大于等于0, 不阻塞。 反之, 没有资源,那么执行doAcquireSharedInterruptibly() 操作,再次尝试,如果失败则阻塞。

doAcquireSharedInterruptibly 方法:

该方法没有被重写,在AbstractQueuedSynchronizer类中

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        /**这个addWaiter 很讲究,大致就是将自己这个线程加入到aqs的阻塞队列的末尾。
         *  Node node = new Node(Thread.currentThread(), mode); 并返回node
         *  如果是第一个阻塞, 会生产一个空head.
         * 不过为了保证线程安全,用了cas操作
         * 先尝试一遍放在队列末尾,如果失败,进入enq方法,
         * 无限循环尝试放在阻塞队列的末尾. (自旋锁)
         * 而且head节点是一个空节点,new Node(); 不存储数据。
        **/
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            /** 无限循环,尝试获取资源,
             *  是否应该阻塞, 调用原语阻塞自身。
             *  当前线程停止在if(xxx && parkAndCheckInterrupt)处。
             *  一旦有资源被释放, 从此处唤醒,继续无限循环尝试获取资源。
            **/
            for (;;) {
                /** 如果当前节点的前一个是head,而head是个标记,不存储数据,
                 *  说明前面没有阻塞的node节点,那么再尝试获取一遍资源。
                **/
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                /** shouldParkAfterFailedAcquire 从阻塞队列末尾往前查看,
                 * 看看前面的资源是否被阻塞,如果被阻塞自己就排在后面也阻塞起来,
                 * 如果没有被阻塞,说明前面这个线程放弃了,从队列中删除
                 * 回到上面循环,重新尝试获取资源。然后再阻塞自己。
                 * 将自身的前一个节点的waitStatus设置为SIGNAL,等待资源唤醒。
                 * 所以node.waitStatus 是指下一个节点需要的状态(等待唤醒, 取消...)
                 * parkAndCheckInterrupt 调用原语,阻塞自己。
                **/
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

到这里阻塞就算是完成了。线程没有获取到资源,全都暂停在parkAndCheckInterrupt。等待其他拥有资源的线程release,那么就会唤醒阻塞的线程,重新回到for(;?。 重新获取资源。

release方法

线程释放资源调用 release方法

    public void release() {sync.releaseShared(1);}

// 该方法没有被重写,在AbstractQueuedSynchronizer类中
    public final boolean releaseShared(int arg) {
       // 无限循环尝试释放资源, state=state+1, 释放成功
       if (tryReleaseShared(arg)) {
            // 释放成功, 唤醒线程抢夺资源。。。
           doReleaseShared();
           return true;
       }
       return false;
   }

和acquire方法相反, release方法释放资源,然后doReleaseShared 唤醒之前被阻塞在parkAndCheckInterrupt 方法的线程。

doReleaseShared 方法:

    private void doReleaseShared() {
        for (;;) {
           Node h = head;
           // 如果阻塞队列中有数据。
           if (h != null && h != tail) {   
               int ws = h.waitStatus;
               // 如果h的状态是等待唤醒
               if (ws == Node.SIGNAL) {
                   if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                       continue;            // loop to recheck cases
                   // 唤醒一个后继节点
                   unparkSuccessor(h);
               }
               // 将设置为共享模式。到了这一步,说明阻塞队列为空。
               else if (ws == 0 &&
                        !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                   continue;                // loop on failed CAS
           }
           // 这次循环头节点没有变化,阻塞队列全部被唤醒。
           if (h == head)                   // loop if head changed
               break;
       }
   }

这个代码也是一个无限循环(闭锁),然后唤醒所有没有放弃的后继节点。线程就会回到上面doAcquire中继续循环。抢占资源或者继续阻塞。等待唤醒。

unpartSuccessor方法:

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        // 修改状态
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        // head.next 才是需要唤醒的线程节点。
        Node s = node.next;
        // 如果next放弃了。修改s位置。
        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;
        }
        // s不为空,执行原语,唤醒阻塞线程。
        if (s != null)
            LockSupport.unpark(s.thread);
    }

到了这里才是真正唤醒线程。而互斥与之差不多,不过是资源数变成1,互斥访问。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值