由浅入深逐步讲解Java并发的半壁江山—AQS(上文)

317 阅读9分钟

1、JUC的由来

synchronized 关键字是JDK官方人员用C++代码写的,在JDK6以前是重量级锁。Java大牛Doug Lea对 synchronized 在并发编程条件下的性能表现不满意就自己写了个JUC,以此来提升并发性能,本文要讲的就是JUC并发包下的AbstractQueuedSynchronizer

在JUC中 CountDownLatch、ReentrantLock、ThreadPoolExecutor、ReentrantReadWriteLock 等底层用的都是AQS,AQS几乎占据了JUC并发包里的半壁江山,如果想要获取锁可以被中断、超时获取锁、尝试获取锁那就用AQS吧

DougLea杰作: HashMap、JUC、ConcurrentHashMap等。

温馨提醒

涉及到AQS重要方法、lock、unlock、CountDownLatch、await、signal几个重要组件的底层讲解所以内容有点长,嫌啰嗦的可直接看几个流程图即可(原图公众号回复【666】即可)。

2、AQS前置知识点

2.1、模板方法

AbstractQueuedSynchronizer是个抽象类,所有用到方法的类都要继承此类的若干方法,对应的设计模式就是模版模式

模版模式定义:一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。

抽象类:

public abstract class SendCustom {
 public abstract void to();
 public abstract void from();
 public void date() {
  System.out.println(new Date());
 }
 public abstract void send();
 // 注意此处 框架方法-模板方法
 public void sendMessage() {
  to();
  from();
  date();
  send();
 }
}

模板方法派生类:

public class SendSms extends SendCustom {

 @Override
 public void to() {
  System.out.println("sowhat");
 }

 @Override
 public void from() {
  System.out.println("xiaomai");
 }

 @Override
 public void send() {
  System.out.println("Send message");
 }
 
 public static void main(String[] args) {
  SendCustom sendC = new SendSms();
  sendC.sendMessage();
 }
}

2.2、LookSupport

LockSupport 是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。常用方法如下:

public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);

park是因为park英文意思为停车。我们如果把Thread看成一辆车的话,park就是让车停下,unpark就是让车启动然后跑起来。

与Object类的wait/notify机制相比,park/unpark有两个优点:

thread为操作对象更符合阻塞线程的直观定义

操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll 唤醒所有等待的线程),增加了灵活性。

park/unpark调用的是 Unsafe(提供CAS操作) 中的 native代码。

park/unpark 功能在Linux系统下是用的Posix线程库pthread中的mutex(互斥量),condition(条件变量)来实现的。mutexcondition保护了一个 _counter 的变量,当 park时,这个变量被设置为0。当unpark时,这个变量被设置为1。

2.3、CAS

CAS 是 CPU指令级别实现了原子性的比较和交换(Conmpare And Swap)操作,注意CAS不是锁只是CPU提供的一个原子性操作指令。

由浅入深逐步讲解Java并发的半壁江山—AQS(上文)

CAS在语言层面不进行任何处理,直接将原则操作实现在硬件级别实现,之所以可以实现硬件级别的操作核心是因为CAS操作类中有个核心类UnSafe类。

关于CAS引发的ABA问题、性能开销问题、只能保证一个共享变量之间的原则性操作问题,以前CAS中写过,在此不再重复讲解。

注意:并不是说 CAS 一定比SYN好,如果高并发执行时间久 ,用SYN好, 因为SYN底层用了wait() 阻塞后是不消耗CPU资源的。如果锁竞争不激烈说明自旋不严重,此时用CAS。

3、AQS重要方法

模版方法分为独占式跟共享式,子类根据需要不同调用不同的模版方法(讲解有点多,想看底层可直接下滑到第四章节)。

3.1 模板方法

3.1.1 独占式获取

3.1.1.1 accquire

不可中断获取锁accquire是获取独占锁方法,acquire尝试获取资源,成功则直接返回,不成功则进入等待队列,这个过程不会被线程中断,被外部中断也不响应,获取资源后才再进行自我中断selfInterrupt()。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}
  1. acquire(arg) tryAcquire(arg) 顾名思义,它就是尝试获取锁,需要我们自己实现具体细节,一般要求是:

如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。

如果当前线程已经保持该锁,则将保持计数加 1,并且该方法立即返回。

如果该锁被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态,此时锁保持计数被设置为 1。

  1. addWaiter(Node.EXCLUSIVE)

主要功能是 一旦尝试获取锁未成功,就要使用该方法将其加入同步队列尾部,由于可能有多个线程并发加入队尾产生竞争,因此采用compareAndSetTail锁方法来保证同步

  1. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

一旦加入同步队列,就需要使用该方法,自旋阻塞 唤醒来不断的尝试获取锁,直到被中断或获取到锁。

3.1.1.2 acquireInterruptibly

可中断获取锁acquireInterruptibly相比于acquire支持响应中断。

1、如果当前线程未被中断,则尝试获取锁。

2、如果锁空闲则获锁并立即返回,state = 1。

3、如果当前线程已持此锁,state + 1,并且该方法立即返回。

4、如果锁被另一个线程保持,出于线程调度目的,禁用当前线程,线程休眠ing,除非锁由当前线程获得或者当前线程被中断了,中断后会抛出InterruptedException,并且清除当前线程的已中断状态。

5、此方法是一个显式中断点,所以要优先考虑响应中断。

 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
     throw new InterruptedException(); // acquireInterruptibly 选择
      interrupted = true; // acquire 的选择

3.1.1.3 tryAcquireNanos

该方法可以被中断,增加了超时则失败的功能。可以说该方法的实现与上述两方法没有任何区别。时间功能上就是用的标准超时功能,如果剩余时间小于0那么acquire失败,如果该时间大于一次自旋锁时间(spinForTimeoutThreshold = 1000),并且可以被阻塞,那么调用LockSupport.parkNanos方法阻塞线程。

doAcquireNanos内部:

  nanosTimeout = deadline - System.nanoTime();
  if (nanosTimeout <= 0L)
      return false;
  if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
      LockSupport.parkNanos(this, nanosTimeout);
  if (Thread.interrupted())
      throw new InterruptedException();

该方法一般会有以下几种情况产生:

在指定时间内,线程获取到锁,返回true。

当前线程在超时时间内被中断,抛中断异常后,线程退出。

到截止时间后线程仍未获取到锁,此时线程获得锁失败,不再等待直接返回false。

3.1.2 共享式获取

3.1.2.1 acquireShared

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

该模版方法的工作:

  1. 调用tryAcquireShared(arg) 尝试获得资源,返回值代表如下含义:

负数表示失败。

0 表示成功,但没有剩余可用资源。

正数表示成功,且有剩余资源。

doAcquireShared作用:

创建节点然后加入到队列中去,这一块和独占模式下的 addWaiter 代码差不多,不同的是结点的模式是SHARED,在独占模式 EXCLUSIVE。

3.1.2.2 acquireSharedInterruptibly

无非就是可中断性的共享方法

public final void acquireSharedInterruptibly(long arg)  throws InterruptedException {
    if (Thread.interrupted()) // 如果线程被中断,则抛出异常
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)  
        // 如果tryAcquireShared()方法获取失败,则调用如下的方法
        doAcquireSharedInterruptibly(arg);
}

3.1.2.3. tryAcquireSharedNanos

尝试以共享模式获取,如果被中断则中止,如果超过给定超时期则失败。实现此方法首先要检查中断状态,然后至少调用一次 tryacquireshared(long),并在成功时返回。否则,在成功、线程中断或超过超时期之前,线程将加入队列,可能反复处于阻塞或未阻塞状态,并一直调用 tryacquireshared(long)。

    public final boolean tryAcquireSharedNanos(long arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquireShared(arg) >= 0 ||
            doAcquireSharedNanos(arg, nanosTimeout);
    }

3.1.3 独占式释放

独占锁的释放调用unlock方法,而该方法实际调用了AQS的release方法,这段代码逻辑比较简单,如果同步状态释放成功(tryRelease返回true)则会执行if块中的代码,当head指向的头结点不为null,并且该节点的状态值不为0的话才会执行unparkSuccessor()方法。

    public final boolean release(long arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

3.1.4 共享式释放

releaseShared首先去尝试释放资源tryReleaseShared(arg),如果释放成功了,就代表有资源空闲出来,那么就用doReleaseShared()去唤醒后续结点。

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

比如CountDownLatch的countDown()具体实现:

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

3.2 子类需实现方法

子类要实现父类方法也分为独占式跟共享式。

3.2.1 独占式获取

tryAcquire 顾名思义,就是尝试获取锁,AQS在这里没有对其进行功能的实现,只有一个抛出异常的语句,我们需要自己对其进行实现,可以对其重写实现公平锁、不公平锁、可重入锁、不可重入锁

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}

3.2.2 独占式释放

tryRelease 尝试释放 独占锁,需要子类实现。

   protected boolean tryRelease(long arg) {
        throw new UnsupportedOperationException();
    }

3.2.3 共享式获取

tryAcquireShared 尝试进行共享锁的获得,需要子类实现。

protected long tryAcquireShared(long arg) {
        throw new UnsupportedOperationException();
    }

3.2.4 共享式释放

tryReleaseShared尝试进行共享锁的释放,需要子类实现。

    protected boolean tryReleaseShared(long arg) {
        throw new UnsupportedOperationException();
    }

3.3 状态标志位

state因为用 volatile修饰 保证了我们操作的可见性,所以任何线程通过getState()获得状态都是可以得到最新值,但是setState()无法保证原子性,因此AQS给我们提供了compareAndSetState方法利用底层UnSafe的CAS功能来实现原子性。

    private volatile long state;

    protected final long getState() {
        return state;
    }

    protected final void setState(long newState) {
        state = newState;
    }

   protected final boolean compareAndSetState(long expect, long update) {
        return unsafe.compareAndSwapLong(this, stateOffset, expect, update);
    }

3.4 查询是否独占模式

isHeldExclusively 该函数的功能是查询当前的工作模式是否是独占模式。需要子类实现。

    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

3.5 自定义实现锁

这里需要重点说明一点,JUC中一般是用一个子类继承自Lock,然后在子类中定义一个内部类来实现AQS的继承跟使用

public class SowhatLock implements Lock
{
 private Sync sync = new Sync();

 @Override
 public void lock()
 {
  sync.acquire(1);
 }

 @Override
 public boolean tryLock()
 {
  return false;
 }

 @Override
 public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
 {
  return sync.tryAcquireNanos(1,unit.toNanos(time));
 }

 @Override
 public void unlock()
 {
  sync.release(1);
 }

 @Override
 public Condition newCondition()
 {
  return sync.newCondition();
 }

 @Override
 public void lockInterruptibly() throws InterruptedException
 {
 }

 private class Sync extends AbstractQueuedSynchronizer
 {
  @Override
  protected boolean tryAcquire(int arg)
  {
   assert arg == 1;
   if (compareAndSetState(0, 1))
   {
    setExclusiveOwnerThread(Thread.currentThread());
    return true;
   }
   return false;
  }

  @Override
  protected boolean tryRelease(int arg)
  {
   assert arg == 1;
   if (!isHeldExclusively())
   {
    throw new IllegalMonitorStateException();
   }
   setExclusiveOwnerThread(null);
   setState(0);
   return true;
  }

  @Override
  protected boolean isHeldExclusively()
  {
   return getExclusiveOwnerThread() == Thread.currentThread();
  }

  Condition newCondition() {
   return new ConditionObject();
  }
 }
}

自定义实现类:

public class SoWhatTest
{
 public static int m = 0;
 public  static CountDownLatch latch  = new CountDownLatch(50);
 public static Lock lock = new SowhatLock();

 public static void main(String[] args) throws  Exception
 {
  Thread[] threads = new Thread[50];
  for (int i = 0; i < threads.length ; i++)
  {
   threads[i] = new Thread(()->{
    try{
     lock.lock();
     for (int j = 0; j <100 ; j++)
     {
      m++;
     }
    }finally
    {
     lock.unlock();
    }
    latch.countDown();
  });
  }
  for(Thread t : threads) t.start();
  latch.await();
  System.out.println(m);
 }
}

篇幅限制,下文请见:由浅入深逐步讲解Java并发的半壁江山—AQS(下文)