Java多线程之AQS

105 阅读8分钟

队列同步器(AQS)

AQS是用来构建锁或者其他同步组件的基础框架,通过int成员变量(state表示同步状态),内置的FIFO队列来实现资源获取线程的排队工作。

主要的使用方式

  • 继承:子类通过继承同步器并实现它的抽象方法来管理同步状态,通过使用同步器提供的3个方法(getState()、setState()、compareAndSetState(int expect,int update))来对同步状态进行更改。推荐将子类定义为自定义同步组件的静态内部类
public class Mutex implements Lock {
    //静态内部类,自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
    ......
     }
    }

AQS和锁之间的关系

  • 同步器(AQS)是实现锁(任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。
  • 锁是面向使用者的,定义了使用者与锁交互的接口(允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。

AQS实现实例和方法解析

这是一个独占锁的实现例子,后续会讲到独占获取锁和共享获取锁

public class Mutex implements Lock {
    //静态内部类,自定义同步器(这里上面有讲到过)
    //这里是将AQS的操作都代理给了Sync这个类了(uu们也可以看一下ReentrantLock的实现,有点像哈)
    private static class Sync extends AbstractQueuedSynchronizer {
        
        //当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        //当状态为0的时候获取锁
        @Override
        public boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        //释放锁 把状态设置为0
        @Override
        protected boolean tryRelease(int releases) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        //返回一个Condition,每个condition都包含了一个condition队列
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    //仅需要将操作代理到Sync即可
    private final Sync sync = new Sync();
    @Override
    public void lock() {
        sync.acquire(1);
    }
    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }
    @Override
    public void unlock() {
        sync.release(1);
    }
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    @Override
    public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException{
        return sync.tryAcquireNanos(1,unit.toNanos(timeout));
    }
}

同步器(AQS)中重写的方法:

protected boolean tryAcquire(int arg)独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,在进行CAS设置同步状态
protected boolean tryRelease(int arg)独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected int tryAcquireShared(int arg)共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败
protected boolean tryReleaseShared(int arg)共享式释放同步状态
protected boolean isHeldExclusively()当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程占用

实现自定义同步组件时,将会调用同步器提供的模板方法(也就是我们用代理类继承AQS时,通过代理类调用以下方法,就能间接的引用到上面的方法):

void acquire(int arg)独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg) 方法
void acquireInterruptibly(int arg)与acquire(int arg)相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException并返回
boolean tryAcquireNanos(int arg,long nanos)在void acquireInterruptibly(int arg)基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,如果获取到了返回true
void acquireShared(int arg)共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步等待
void acquireSharedIntteruptibly(int arg)与acquireShared(int arg) 相同,该方法响应中断
boolean tryAcquireSharedNanos(int arg,long nanos)在acquireSharedInterruptibly(int arg)基础上加上了超时限制
boolean release(int arg)独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
boolean releaseShared(int arg)共享式的释放同步状态
Collection getQueuedThreads()获取等待在同步队列上的线程集合

队列同步器的实现分析

队列同步器是通过同步队列、独占式同步状态获取与释放、共享式同步状态获取与释放以及超时获取同步状态等同步器的核心数据结构与模板方法来实现的。

同步队列

同步器依赖内部的同步队列(FIFO的双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构成一个节点并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会将首节点中的线程唤醒,使其再次尝试获取同步状态。

独占获取锁

当一个线程获取同步状态(或者锁),其他线程会获取同步状态失败,这个时候就会通过CAS(保证线程安全,以及保证获取同步状态的有序性)将这个线程等信息构成一个节点加入队列的尾部,不断地通过自旋询问前一个节点是否为头节点,如果前一个节点为头节点,那么由于首节点在释放同步状态之后,就会唤醒后一个节点,而后继节点会在自己获取同步状态成功后将自己设置为头节点。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    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)) {
                //通过CAS的方式来存入尾节点,保证线程安全添加
                pred.next = node;
                return node;
            }
        }
    	//如果尾节点不为空或者尾节点为空,此时由于CAS添加至尾节点失败,那么
    	//就进入到死循环添加到尾节点当中
        enq(node);
        return node;
    }
    //通过循环将该节点添加到尾节点(保证获取不到同步状态的线程都能加入到尾节点中)
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;
                }
            }
        }
    }

独占释放锁

当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态后,会唤醒其后继节点。

public final boolean release(int arg) {
       if (tryRelease(arg)) {
           Node h = head;
           if (h != null && h.waitStatus != 0)
   //使用LockSupport来唤醒处于等待状态的线程
               unparkSuccessor(h);
           return true;
       }
       return false;
   }
共享式获取锁和释放锁
  • 共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。
  • 共享式访问资源时,其他共享式的访问均被允许,而独占式被阻塞。
  • 独占式访问资源时,同一时刻,其他访问均被阻塞
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);
                  //方法返回值大于等于0的时候,表示成功获取同步状态
                  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);
      }
  }

在共享式获取自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int args)方法返回大于等于0。

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

该方法释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件,它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过CAS和循环来保证的,因为释放同步状态的操作会同时来自多个线程。

总结

AQS 主要依赖于两个东西一个是int state变量用来保证同步状态(这个是用volatile修饰的变量QAQ),其次就是FIFO的双向队列。理解这两个东西的作用 其实就已经事半功倍了。其余的源码看多几遍就okk了。分别在理解一下独占获取释放锁和共享获取释放锁。

这些都是鄙人通过阅读Java并发编程的艺术(小蓝书)的笔记QAQ