Java源码学习之高并发编程基础——AQS源码剖析之阻塞队列(上)

1.前言&目录

前言:

AQS的全称是AbstractQueuedSynchronizer,抽象队列同步器。它主要是对多线程竞争锁的过程中,以约定的规则让这些抢不到锁的线程去“乖乖排队”,这个规则就是FIFO——先进先出。

AQS全称可以拆分为抽象、队列、同步器,而且事实上AQS也是这么做的。

  • 抽象之处在于AQS是一个abstract关键字修饰的类,这让我们想起了Java的模板设计模式,这种模式下的类一般是抽象的,并且其某些方法是钩子的或者是抽象的,需要留给子类去实现。
  • 队列,则是AQS中存储等锁线程节点的数据结构,是使用双向链表组成的一个队列,其公平之处体现在所有线程都是按照先来后到的顺序入队,是FIFO的公平队列。
  • 同步器,同步就是做一件事情需要一件件的来,不能急不能躁,在AQS体现在一把锁释放后,每次只能有一个排名最靠前的等锁节点能抢到锁,其他没抢到锁的线程需要继续在队列里等待。

AQS的同步阻塞队列模型 

 

由于AQS源码较多,本文主要讲解AQS的核心能力,即队列的入队和出队操作。但是在AQS中又存在独占锁和共享锁,因此阻塞队列的讲解将分为上下两篇文章解读,此篇为上文——以独占锁为主进行源码解读。

目录:

1.前言&目录

2.为什么使用AQS

3.源码分析

3.1 AQS类介绍

3.2 Node节点类介绍

3.3 入队

 3.3.1 tryAcquire源码剖析

3.3.2 addWaiter源码剖析

3.3.3 acquireQueued源码剖析

3.4 出队

3.4.1 tryRelease源码剖析

3.4.2 unparkSuccessor源码剖析

4. 简单案例 

5.总结

2.为什么使用AQS

在程序界中,以公平与否的角度看,存在公平锁和非公平锁。在Java1.5以前,锁的实现只能通过synchronized关键字去定义,但它是非公平锁,这种情况就造成能否获取到锁似乎完全看运气了,而AQS正是为实现公平锁而生,它能让多线程以公平的方式去排队获取锁。

在Java1.5前,可以通过对方法、代码块等使用synchronized关键字确定一个并发区域——即用锁锁住某个区域、同一个时刻只能有一个线程能访问,但是它并不是一个公平锁。

因此为了弥补只有非公平锁的缺陷,在Java1.5版本推出了AQS,通过它内部维护的双向链表组成的队列可以实现公平锁。

此外,更重要的一点是AQS的锁实现比Synchronized锁更轻量化,Synchronized会导致获取不到锁的线程直接开始阻塞,而在acquireQueued源码剖析这个章节分析了,AQS是会有一个二次获取锁的过程,并不是抢不到锁就直接被阻塞。从这点考虑,AQS组成的锁机制的确更轻量化。

3.源码分析

AQS以锁的占有来说,又分为独占锁和共享锁,本文将以独占锁去讲解源码,因为独占锁的使用频率更高。

3.1 AQS类介绍

 在讲解AQS的作用以前,我们需要先认识这个AQS类的基本结构以及其中的一些重要成员属性。

在以下伪代码中可以看到,AQS确实是通过abstract关键字修饰的,这也决定了不能在代码里通过new关键字去实例化一个AQS对象。但是可以通过继承它,实例化一个子类去继承AQS提供的方法(功能)。

前言中大致介绍过,在多个线程竞争同一把锁的过程中,抢不到锁的线程都会被封装为一个节点,这个节点正是Node节点,然后以双向链表的形式实现一个等待锁的队列。正如下所示,AQS中存在head节点和tail节点,它们就是双向链表中的头节点和尾节点。对于双向链表,笔者也写了一篇介绍双向链表的文章,如果还不是很熟悉的话可以去看看。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
    protected AbstractQueuedSynchronizer() {}
    private transient volatile Node head;// 队列头节点
    private transient volatile Node tail;// 队列尾节点
    private volatile int state;// 同步状态
}

此外,AQS还有一个int类型的state,它会被子类用于判断当前队列是否正在同步中(即是否有线程在等待锁),主要分为0和非0两种情况,通常情况下,0表示当前线程可以“霸占”锁,非0则表示锁已经被其他线程持有了。

注意到,头节点、尾节点、state三个成员变量都是使用volatile关键字修饰的,在AQS的作用就是线程可见性,即一个线程修改了它们的值,其它线程能立刻感知并读取最新的值。

3.2 Node节点类介绍

Node节点是队列的元素,它的作用就是将等锁线程封装起来,并且通过维护前驱节点和后驱节点确定某个线程的排队顺序。当锁被释放时,排最前面的等锁节点中的线程将会得到唤醒去获取锁。

欲先善其事,必先利其器,想要掌握AQS的核心原理,我们也必须提前掌握这个Node节点的特征,下面是Node节点类的主要源码以及几个核心关键点:

  • 由于Node节点是双向链表的元素,因此它拥有一个volatile修饰的prev变量和next变量(下文统称前驱节点、后驱节点)指向前后节点。
  • Node节点通过volatile Thread thread去保存当前线程对象,表示将当前线程转变成一个等锁节点,然后入队到队列中去排队等待锁的释放。
  • Node节点通过volatile int waitStatus变量控制当前Node节点是否可用,只有当它是小于等于0的时候表示可用,而大于0则表示当前等待锁的节点不可用了,可能需要被移除。以独占锁为例,它具体有0,-1,1三个值,0是不指定状态初始化Node的值,-1(SIGNAL)则表示当前节点在等待信号获取锁,1(CACELLED)则是当前节点被取消,不再是一个有效节点。
  • 独占锁和共享锁的标记,Node SHARED是共享锁的标记,Node EXCLUSIVE是独占锁的标记,只能通过构造函数设置,不过它们仅仅起到一个标记作用。
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
  static final class Node {
     static final Node SHARED = new Node(); // 共享锁的标记
     static final Node EXCLUSIVE = null; // 独占锁的标记
     static final int CANCELLED =  1; // 节点被取消
     static final int SIGNAL    = -1; // 节点在等待信号
     static final int PROPAGATE = -3; // 该状态是共享锁使用的 
     volatile int waitStatus;// 节点等待状态,默认是-1
     volatile Node prev; // 当前等锁节点的前驱节点
     volatile Node next; // 当前等锁节点的后驱节点
     volatile Thread thread; // 当前线程
     Node nextWaiter; // 共享锁需要用到的变量
     Node(){};     
     Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode; // 共享锁会使用到这个变量判断
        this.thread = thread;
     }
     // 获取当前节点的前驱节点,前驱节点空时会报错
     final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
           throw new NullPointerException();
        else
           return p;
     }
  }
}

而Node nextWaiter这个变量则是共享锁使用的,由于本文以讲解独占锁为主,不会深入讲解此变量的作用。但是请记住,它是为共享锁而生即可。

此外,Node类还提供了predecessor()方法,它的作用是返回当前Node节点的前驱节点,如果前驱节点为null时会抛出空指针异常。

经过AQS类介绍和Node节点类介绍后,我们就可以讲解AQS中的入队和出队了,这两个功能是AQS中最为的核心方法,因为入队和出队分别代表着抢不到锁的线程的排队、排队中的线程获取锁后出队。

3.3 入队

AQS的入队,更为清晰的描述为,当某个线程获取不到锁时,通过封装成Node节点加入到队列中(即通过维护AQS实例的头节点和尾节点,去让该等锁线程节点在正确位置上排队)。

如下伪代码所示,AQS#acquire(int arg)方法涵盖了锁的获取以及获取锁失败到入队的操作。涉及了三个方法:

  • tryAcquire(int arg)是AQS中留给子类实现的钩子方法,它的作用就是尝试获取锁并返回是否获取到了锁,注意传入的arg参数就是用来留给子类判断当前队列同步状态的——AQS的state变量。
  • addWaiter(Node.EXCLUSIVE)则是AQS中将调用tryAcquire方法失败(返回false则是当前线程获取不到锁)的线程封装为Node节点入队的操作。
  • acquireQueued(addWaiter(Node.EXCLUSIVE), arg))则是AQS中将刚刚通过addWaiter方法入队的等锁节点以不间断的方式(即自旋)或者阻塞的方法去获取锁的操作。
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
    public final void acquire(int arg) {
       // tryAcquire是模板方法,需要留给子类实现
       if (!tryAcquire(arg) &&
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();
    }
    static void selfInterrupt() { // 让当前线程尝试中断自己
        Thread.currentThread().interrupt();
    }
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
}

如果经过tryAcquire获取锁失败并且,acquireQueued也失败的话,则会调用当前线程的interrupt()方法尝试让自己“中断”。

 3.3.1 tryAcquire源码剖析

tryAcquire方法是AQS的钩子方法,作用是获取锁并返回结果,是需要留给子类去实现。因此以ReentrantLock.FairSync重写的tryAcquire方法为例讲述获取锁的过程。

在以下伪代码可以看到,ReentrantLock.FairSync这个子类(实际上它继承的父类是内部类Sync,它又继承了AQS)重写的tryAcquire方法就是根据AQS的state变量值等于0时,由于存在多线程并发抢锁的过程,因此会通过CAS(因为state是volatile修饰的,CAS的操作可以保证正确性)的方法尝试去将AQS的state从预期的0改为传入的值(实际是1)。

一切顺利的话,会调用setExclusiveOwnerThread(current)方法将当前线程对象(引用变量)设置给AQS继承的AbstractOwnableSynchronizer的Thread exclusiveOwnerThread成员变量(独占锁才会用到)。然后返回true给调用的方法,即AQS#acquire方法表示当前线程获取到锁了。如果返回false则表示当前线程没获取到锁。

public class ReentrantLock implements Lock{
    private final Sync sync;
    static final class FairSync extends Sync {
       final void lock() {
            acquire(1);
       }
       protected final boolean tryAcquire(int acquires) {
          final Thread current = Thread.currentThread();
          int c = getState();// 获取AQS的state变量值
          if (c == 0) {
             // 用AQS的state=0?判断是否此前没有其他线程拿到锁
             if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
             }
          }
          // ...可重入锁的操作
          return false;
        }
    }
}

由此可见,“锁”的这个概念和操作,其实存在于AQS的子类而非AQS本身的,即AQS并不负责获取锁的操作。

3.3.2 addWaiter源码剖析

当调用AQS#acquire方法时,会先调用AQS子类的tryAcquire方法尝试获取锁,拿到锁则返回true,跳出此方法。拿不到锁则返回false接着调用addWaiter方法。

 其实addWaiter方法只做了一件事情,就是当前线程和独占锁的标记封装为一个Node节点(下文统称等锁节点)并将它添加到AQS队列的尾部——此为AQS阻塞队列的特点:FI(先进)。

如果此时尾节点不为null即当前队列不为空,先调用一次CAS操作尝试将等锁节点设置为新的尾节点。为什么这里也要通过CAS操作呢?因此在AQS类介绍的时候讲过了,头节点和尾节点和state等变量都是volatile修饰的,为了保证多个线程同时操作这些变量时能立刻感知到修改后的值。

也就是说,其实这个addWaiter方法会存在并发操作,因为它的入口之一就是AQS#acquire方法——会在AQS子类中获取锁的方法中被调用,即多线程可能同时调用某个子类的获取锁方法。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
   private Node addWaiter(Node mode) {
     Node node = new Node(Thread.currentThread(), mode);
     Node pred = tail;
     if (pred != null) {
         node.prev = pred;
         // CAS的设置队列尾节点
         if (compareAndSetTail(pred, node)) {
             pred.next = node;
             return node;
         }
     }
     enq(node);
     return node;
    }
   // 通过死循环添加节点入队的方法
   private Node enq(final Node node) {
     for (;;) {
        Node t = tail;
        if (t == null) { // 初次进入会创建一个“空”头节点,作用仅仅是作为队列头节点
           if (compareAndSetHead(new Node()))
               tail = head;
           } else {
               node.prev = t;
               // CAS的将节点添加到尾部
               if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
               }
           }
       }
    }
}

 如果某个线程第一次CAS操作不成功或者队列本来就是空的,那么将会接着调用enq(node)方法再次将当前等锁节点入队。其细节如下:

  • 如果队列为空,则新建一个没有持有线程对象(引用变量)的“空头”节点并通过CAS操作设置为队列的头节点——即队列的初始化。也就是说,等锁节点实际上是队列的从第二位开始排队的,第一的永远是这个“空头”头节点,它在这里仅仅起到了“牵头”的作用。
  • 队列初始化以后,当前等锁节点同样会通过CAS操作尝试将自己设置为队列的尾节点,如果尾节点添加成功则返回并且退出enq方法——入队的操作通过CAS操作,岂不是也并非的绝对公平?其实,公平与否只是相对的一种程度,如果大多数情况下入队是按顺序,那就可以认定是公平的。

注意到,这个enq方法是一个自旋的操作(即死循环),它只有成功通过CAS方法将当前等锁节点添加到队列尾部才会停止自旋、退出循环。也就是说若多个线程同时进入到此方法想入队,也需要那么点运气~~~。

3.3.3 acquireQueued源码剖析

当一个获取不到锁的线程通过addWaiter方法成功入队到队列尾部后,该线程实际上还在正常运行着,因此仅仅入队是还不够的,在acquireQueued方法里还会给等锁线程尝试阻塞,直到有人释放锁了并通知它“不要在阻塞了,你可以继续工作了——去拿锁吧!”。

 acquireQueued方法也是通过自旋的方式去进一步操作该等锁线程,主要有两个操作:

  • 会判断当前等锁节点是不是排在“空头”头节点后面,即实际上的排第一位的等锁节点,并且还会尝试调用一次AQS子类的tryAcquire方法尝试二次获取锁,如果拿锁成功,则通过setHead方法将该等锁节点重新设置为新的“空”头节点(把绑定的线程变量也清空了。)——二次获取锁的过程,这一点分析就会比Synchronized更轻量化。
  • 如果二次获取锁失败或者该等锁节点不是排第一位的,则通过shouldParkAfterFailedAcquire方法做一个预判——是否需要将该等锁的线程阻塞(阻塞等锁线程可能是为了不必要的自旋,减少CPU的切换)。预判过程就是将当前等锁节点的前驱节点的状态判断是否是Node.SIGNAL,是的话直接返回true表示需要阻塞。如果前驱节点已经被取消了,则向前遍历找到新的非取消的前驱节点,取消的节点就会被GC回收(因为没有引用了)。最后,将一个状态为0(节点初始化时为0)或者是-3(共享锁用到的)通过CAS操作将它设置为Node.SIGNAL并在下一次的自旋中的预判中返回true表示需要阻塞了。
public abstract class AbstractQueuedSynchronizer{
       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);
        }
    }
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
           // 此节点设置 status=-1,表示当前线程需要被安全的阻塞...
            return true;
        if (ws > 0) {
            // 等锁节点的前驱节点已经是CANCEL状态了,因此要将该不可工作的前驱节点去除掉
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // waitStatus 必须为 0 或 PROPAGATE。指示我们需要信号,
           // 但暂时不要阻塞线程。调用方需要重试以确保在阻塞前无法获取。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    private final boolean parkAndCheckInterrupt() {
       LockSupport.park(this);// 将Thread.currentThread()阻塞中。
       return Thread.interrupted();
    }
}

 而parkAndCheckInterrupt()则会通过Thread.currentThread()获取当前的线程将其进行阻塞,也就是说。也就是说如果一个等锁线程经过tryAcquire——addWaiter——tryAcquire的调用链还无法获取到锁的话,则会阻塞在parkAndCheckInterrupt()方法中。

那么,该等锁的线程什么时候被唤醒呢?答案是在出队中调用方法unparkSuccessor被唤醒,唤醒后重新进入自旋获取锁的过程。

3.4 出队

AQS的出队,指的是实际上排在第一位的等锁线程节点(队列头部总是一个“空头”头节点,不保存线程变量,它的后继节点才是真正的等待节点),等待获取到了锁以后,将自己从队列移除出去的操作。

按照此定义,那么出队的场景在acquireQueue方法也发生过,就是在还没有预判到需要阻塞线程时,恰好此时第一的等锁节点等到了其他线程持有的锁的释放并获取后,会成为新的头节点,并置空保存的线程变量,这也是一种出队的情况。不过这种情况,概率或许比较少,并不常见,因为需要一个线程刚入队,另一个线程就释放锁。

常见的出队情况是持有锁的线程主动调用AQS#release的释放锁的操作。这个过程,也是主要做了两件事情:

  • 通过调用AQS子类重写的tryRelease方法判断是否释放锁成功,成功释放锁以后才会继续调用unparkSuccessor(h)方法。
  • 根据此时头节点是否为null,并且waitStatus是否不等于0,实际上如果队列中有等锁节点,在acquireQueued源码剖析中已经分析过,“空头”头节点的waitStatus会被设置为SIGNAL(-1),表示需要等待信号唤醒。而这个信号,正是来这这里——锁释放的时刻。在unparkSuccessor方法中,则会从“空头”头节点的后继节点开始,找到最前面的一个没有被取消等锁节点,将它从阻塞中唤醒转为不阻塞。
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
       public final boolean release(int arg) {
        // 调用子类重写的tryRelease方法释放锁。
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
}

3.4.1 tryRelease源码剖析

tryRelease和tryAcquire一样,都是AQS中需要留给子类实现的钩子方法,它的主要作用是释放独占锁。

还是以ReentrantLock为例,它的成员变量Sync sync是AQS的子类,ReentrantLock是通过unlock()方法释放锁的,在这里会先去调用AQS的release方法,再回调到sync#tryRelease方法去释放锁。

可以看到,释放锁的过程,先判断当前的线程和绑定在AQS中获得独占锁的那个线程是否是同一个对象,如果不是则抛出异常。

接着以AQS的state变量作为判断依据,如果此时state-1=0的话,则将free变量设置为true并且将AQS中绑定的获取独占锁的线程变量置为null,最后重新设置此时AQS的state为0,表示此时可以重新去竞争锁了。如果state-1!=0,则表明此时的锁还在某个线程中持有着,将则返回false表示锁没有被成功释放。

public class ReentrantLock implements Lock {
   private final Sync sync;
   public void unlock() {
        sync.release(1);// 调用AQS子类释放锁
   } 
   abstract static class Sync extends AbstractQueuedSynchronizer {
       protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                // 还是以最终的AQS的state变量当前能否获取到锁的依据 
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
      }
   }
}

3.4.2 unparkSuccessor源码剖析

unparkSuccessor方法则是从“空头”头节点的后驱节点开始,找到排名最靠前的、仍然有效(即不是被取消了、waitStatus是CANCEL(1))的等锁节点,将其从阻塞状态唤醒起来去重新获取锁。

 在出队方法入口处之一AQS#release中,会将此时的头节点(waitStatus是SIGNAL(-1))作为入参调用unparkSuccessor(Node node)方法,在这里会先通过CAS尝试更改此时头节点的waitStatus为0,那么这个操作有什么作用呢?其实这只是一个简单的清除“信号”的标记,在acquireQueued方法中,队列的头节点的waitStatus将会被设置为SIGNAL——表示需要等待唤醒的信号。如今已经进来了,自然要将该信号清除。

然后从“空头”头节点后继节点(后继节点们才保存了线程变量,它们才是真正的等锁节点)开始,如果此时的节点是null或者是已经被取消了,就会从队列尾部倒序遍历找到一个离“空头”头节点最近的一个有效节点(waitStatus<=0,对于独占锁来说,一般的等锁节点的等待状态都是初始化的0,只有头节点才会升级为-1。) 

private void unparkSuccessor(Node node) {
     // 进入unparkSuccessor方法的前提是,node<=-1,即SIGNAL或者PROPAGATE
     int ws = node.waitStatus;
     if (ws < 0)
         // 会重新设置当前“空头”头节点的waitStatus等于0,即清除当前头节点需要
         // 信号的标志 
         compareAndSetWaitStatus(node, ws, 0);
     // 如果此时“空头”头节点的后驱节点是null或者已经被取消了,则倒序从队列
     // 找到离“空头”头节点最近的有效等锁节点,然后将其唤醒
     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);
}

当最近的有效等锁节点找到以后,就会调用LockSupport.unpark(s.thread),将此时等锁节点绑定的线程从阻塞中唤醒至非阻塞——在acquireQueued源码剖析分析过,只有实际排名第一的等锁节点并且重新二次调用tryAcquire方法获得锁以后才会退出自旋操作,否则当前线程将最终会被阻塞住。

即这些没能重新获取锁得等锁线程会在这里逐个逐个的被唤醒,最终回到acquireQueued方法的自旋中重新尝试获取锁,获取到锁以后,原来的头节点会被抛弃,该节点会升级为新的“空头”头节点,其后驱节点就是下一个等待唤醒的等锁节点——即先入队的节点也是先出队的节点,AQS的FIFO就这么实现了。

4. 简单案例

经过3.源码分析分析这个章节,我们知道了AQS对于等锁节点的入队和出队操作,是公平的、FIFO先进先出的。即可通过继承AQS可以实现一个公平锁,接下来将用案例去说明这一点。

还是以 ReentrantLock的公平锁为例,在下面案例中,实现Runnable接口的TestRunnable实例的run方法,会先上锁接着输出当前线程的名称,最后释放锁。然后将其实例传入Thread实例中,调用start方法去启动线程。由于start方法中调用run方法是由本地C方法执行的,因此不能确定启动线程后,它就能立刻工作。

但是由于这里的run方法并不复杂,仅仅是输出一条语句。即如果以顺序的方法创建Thread实例并启动线程的话,很可能输出语句中的线程名称也是顺序的。果然,经过多次实验,输出语句中线程名称是顺序的概率超过了50%,因此可以认定AQS提供了一个实现公平锁的解决方案。

public class AqsLearn1 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock(true);
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new TestRunnable(reentrantLock));
            thread.setName("线程" + (i + 1));
            thread.start();
        }
    }
    static class TestRunnable implements Runnable{
        private ReentrantLock reentrantLock;
        public TestRunnable(ReentrantLock reentrantLock){
            this.reentrantLock = reentrantLock;
        }
        @Override
        public void run() {
            try {
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName() + "正在工作");
            }finally {
                reentrantLock.unlock();
            }
        }
    }
}
----------------输出结果----------------------
线程1正在工作
线程2正在工作
线程3正在工作
线程5正在工作
线程4正在工作
----------------输出结果----------------------

 在Java1.5以前是通过synchronized关键字去定义一个并发区域的,它的锁粒度更粗、并且不是公平的,锁的获取几乎可以说是“完全靠运气”。

因此,从下面synchronized锁的案例中可以看到,经过验证,输出结果中处理线程1是基本都第一个输出,后面的四个通常是毫无顺序、规律可言,即可以认为synchronized是一个非公平锁。

public class AqsLearn1 {
    public static void main(String[] args) {
        Object synchronizedLock = new Object();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new TestRunnableBySynchronized(synchronizedLock));
            thread.setName("线程" + (i + 1));
            thread.start();
        }
    }
    static class TestRunnableBySynchronized implements Runnable{
        private Object lock;
        public TestRunnableBySynchronized(Object lock){
            this.lock = lock;
        }
        @Override
        public void run() {
            synchronized (lock){
                System.out.println(Thread.currentThread().getName() + "正在工作");
            }
        }
    }
}
----------------输出结果----------------------
线程1正在工作
线程4正在工作
线程3正在工作
线程5正在工作
线程2正在工作
----------------输出结果----------------------

5.总结

AQS实际上只是提供了一套通过双向链表组成的同步等待(锁)队列的解决方案,锁的获取以及释放还是需要留给AQS子类实现,AQS只不过是负责将那些获取不到锁的线程去封装为一个Node节点的保存在阻塞队列中。

在ReentrantLock.FairSync的例子中,多个线程并发调用AQS#tryAcquire方法时,只能有一个线程能获取到锁,其余的都将会入队等待锁的释放。并且是先入队的节点会先获取锁,即该队列是一个FIFO(先进先出)的,基于此特性决定了可以继承AQS可以实现公平锁。

当持有锁的线程成功释放锁以后,会进入到AQS#release方法中,在这里则会从队列中找到排名最靠前、没有被取消的等锁节点,将其从阻塞状态唤醒至非阻塞,再次去重新获取锁。

也就是说,仅以独占锁为例,AQS最重要的两个能力就是维持等锁节点的公平入队、公平出队。

  • 16
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值