深入理解AbstractQueuedSynchronizer

我欲只争朝夕
发布于 2023-11-9 11:46
浏览
0收藏

前言

建议先看一下上一个分享:​​CAS实现原理​


JUC中的许多并发工具类ReentrantLock,CountDownLatch等的实现都依赖AbstractQueuedSynchronizer 

深入理解AbstractQueuedSynchronizer-鸿蒙开发者社区

AbstractQueuedSynchronizer定义了一个锁实现的内部流程,而如何上锁和解锁则在各个子类中实现,典型的模板方法模式

模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤


下面举个简单的例子说明一下

咖啡因饮料抽象类


public abstract class CaffeineBeverage {

  final void prepareRecipe() {
      //煮水
      boilWater();
      //酿造
      brew();
      //导入杯子
      pourInCup();
      //添加调味料
      addCondiments();
  }

  protected abstract void brew();

  protected abstract void addCondiments();

  private void boilWater() {
      System.out.println("Boiling water");
  }

  private void pourInCup() {
      System.out.println("Pouring into cup");
  }
}


茶饮料


public class Tea extends CaffeineBeverage{
  @Override
  protected void brew() {
      System.out.println("Steeping the tea");
  }

  @Override
  protected void addCondiments() {
      System.out.println("Adding Lemon");
  }
}


咖啡饮料


public class Coffee extends CaffeineBeverage{
  @Override
  protected void brew() {
      System.out.println("Dripping Coffee through filter");
  }

  @Override
  protected void addCondiments() {
      System.out.println("Adding Sugar and Milk");
  }
}


测试类


public class BeverageTestDrive {

  public static void main(String[] args) {
      Tea tea = new Tea();
      /*Boiling water
      Steeping the tea
      Pouring into cup
      Adding Lemon*/
      tea.prepareRecipe();
  }
}


这里有几个注意的地方


1.  boilWater(煮水)和pourInCup(导入杯子)的修饰符为private,表示不让用子类直接用这些方法,也不能重写

2. prepareRecipe(准备食物)方法的修饰符为protected(不写默认为protected)和final,目的是为了让子类用但是不允许子类覆盖、

3. brew(酿造)和addCondiments(添加调味料)的修饰符为abstract,表示子类必须自己定义实现

属性介绍

基于jdk1.8.0_20,AQS内部维护一个FIFO的队列,通过该队列来实现线程的并发访问控制,队列中的元素是一个Node节点


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 CONDITION = -2;
  static final int PROPAGATE = -3;

  //当前节点的状态
  volatile int waitStatus;

  //前继节点
  volatile Node prev;

  //后继节点
  volatile Node next;

  //当前线程
  volatile Thread thread;

  //存储在condition队列中的后继节点
  Node nextWaiter;

}


这里先说一下park方法,后面会提到,park方法是Unsafe类中的方法,与之对应的是unpark方法。简单来说,当前线程如果执行了park方法,也就是阻塞了当前线程,反之,unpark就是唤醒一个线程。


waitStatus表示节点的状态,包含的状态有


  1. CANCELLED  1  当前节点被取消
  2. SIGNAL  2  表示当前节点的的后继节点将要或者已经被阻塞,在当前节点释放的时候需要unpark后继节点
  3. CONDITION  -2  表示当前节点在等待condition,即在condition队列中
  4. PROPAGATE  -3  表示releaseShared需要被传播给后续节点(仅在共享模式下使用)
  5. 0  当前节点在队列中等待获取锁


再来看AbstractQueuedSynchronizer这个类的属性


//等待队列的头节点
private transient volatile Node head;

//等待队列的尾节点
private transient volatile Node tail;

//加锁的状态,在不同子类中有不同的意义
private volatile int state;

深入理解AbstractQueuedSynchronizer-鸿蒙开发者社区

AQS提供了独占锁和共享锁两种方式,每种方式都有响应中断和不响应中断的区别,所以AQS的锁可以分为如下四类


  1. 不响应中断的独占锁(acquire)
  2. 响应中断的独占锁(acquireInterruptibly)
  3. 不响应中断的共享锁(acquireShared)
  4. 响应中断的共享锁(acquireSharedInterruptibly)

而释放锁的方式只有两种


  1. 独占锁的释放(release)
  2. 共享锁的释放(releaseShared)


我们只看一下不响应中断的独占锁,其他的类似

不响应中断的独占锁

从加锁这一部分开始


//调用ReentrantLock公平锁的lock()方法其实就是调用acquire(1);
public final void acquire(int arg) {
   if (!tryAcquire(arg) &&
       acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
       selfInterrupt();//当前线程将自己中断
}


1. 先尝试获取,如果获取到直接退出,否则进入2

2. 以独占模式将线程包装成Node放到队列中

3. 因为在放到队列的过程中,锁有可能释放了,所以再尝试获取,如果获取到锁则将当前节点设为head节点,退出,否则进入4

4. 一直尝试获取,如果满足阻塞条件,则阻塞,如果被唤醒,则继续尝试获取

5. 当获取到锁时,如果线程获取过程发生中断,则最后将中断补上,即执行selfInterrupt()方法

深入理解AbstractQueuedSynchronizer-鸿蒙开发者社区

1. tryAcquire(尝试获取锁)是让子类实现的


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


这里通过抛出异常来告诉子类要重写这个方法,为什么不将这个方法定义为abstract方法呢?因为AQS有2种功能,独占和共享,如果用abstract修饰,则子类需要同时实现两种功能的方法,对子类不友好


2

//1.尝试将新节点通过CAS的方式设置为尾节点,如果成功,返回附加着当前线程的节点
//2.如果CAS操作失败,则调用enq方法,循环入队直到成功
private Node addWaiter(Node mode) {
   //1
   Node node = new Node(Thread.currentThread(), mode);
   Node pred = tail;
   if (pred != null) {5
       node.prev = pred;
       if (compareAndSetTail(pred, node)) {
           pred.next = node;
           return node;
       }
   }
   //2
   enq(node);
   return node;
}


//循环插入队尾直到CAS成功
private Node enq(final Node node) {
   for (;;) {
       Node t = tail;
       if (t == null) { // 必须初始化
           if (compareAndSetHead(new Node()))
               tail = head;
       } else {
           node.prev = t;
           if (compareAndSetTail(t, node)) {
               t.next = node;
               return t;
           }
       }
   }
}


3.4

//自旋获取锁,直到获取锁成功,或者异常退出
//但是并不是busy acquire,因为当获取失败后会被挂起,由前驱节点释放锁时将其唤醒
//同时由于唤醒的时候可能有其他线程竞争,所以还需要进行尝试获取锁,体现的非公平锁的精髓。
final boolean acquireQueued(final Node node, int arg) {
   boolean failed = true;
   try {
       boolean interrupted = false;
       for (;;) {
           //获取前继节点
           final Node p = node.predecessor();
           //node节点的前继节点是head节点,尝试获取锁
           //如果成功说明head节点已经释放锁了
           //将node设为head开始运行
           if (p == head && tryAcquire(arg)) {
               setHead(node);
               p.next = null; // help GC
               failed = false;
               return interrupted;
           }
           //1.获取锁失败后是否可以挂起
           //2.如果可以挂起,则挂起当前线程(获取锁失败的节点)
           if (shouldParkAfterFailedAcquire(p, node) &&
               parkAndCheckInterrupt())
               interrupted = true;
       }
   } finally {
       if (failed)
           cancelAcquire(node);
   }
}


//根据前继节点的状态,是否可以挂起当前获取锁失败的节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
   int ws = pred.waitStatus;
   //前继节点释放时会unpark后继节点,可以挂起
   if (ws == Node.SIGNAL)
       return true;
   if (ws > 0) {
       //将CANCELLED状态的线程清理出队列
       do {
           node.prev = pred = pred.prev;
       } while (pred.waitStatus > 0);
       pred.next = node;
   } else {
       //将前继节点的状态设置为SIGNAL,代表node需要被运行
       compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
       //更新可能失败,所以不能直接返回true
   }
   return false;
}


//挂起线程,返回是否被中断过
private final boolean parkAndCheckInterrupt() {
   LockSupport.park(this);
   return Thread.interrupted();
}


5将中断补上

static void selfInterrupt() {
 Thread.currentThread().interrupt();
}


处理异常退出的node

private void cancelAcquire(Node node) {
   if (node == null)
       return;

   //设置该节点不再关联任何线程
   node.thread = null;

   //跳过CANCELLED节点
   Node pred = node.prev;
   while (pred.waitStatus > 0)
       node.prev = pred = pred.prev;

   //过滤后的前继节点的后继节点
   Node predNext = pred.next;

   //设置状态为取消状态
   node.waitStatus = Node.CANCELLED;

   //当前节点是tail,尝试更新tail节点,设置tail为pred
   //更新失败则返回,成功则设置tail的后继节点为null
   if (node == tail && compareAndSetTail(node, pred)) {
       compareAndSetNext(pred, predNext, null);
   } else {
       //如果当前节点不是head的后继节点:
       //判断当前节点的前继节点的状态是否是SIGNAL,如果不是则尝试设置前继节点的状态为SIGNAL;
       //上面两个条件如果有一个返回true,则再判断前继节点的thread是否不为空;
       //若满足以上条件,则尝试设置当前节点的前继节点的后继节点为当前节点的后继节点
       //也就是相当于将当前节点从队列中删除
       int ws;
       if (pred != head &&
           ((ws = pred.waitStatus) == Node.SIGNAL ||
            (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
           pred.thread != null) {
           Node next = node.next;
           if (next != null && next.waitStatus <= 0)
               compareAndSetNext(pred, predNext, next);
       } else {
           //如果是head的后继节点或者状态判断或设置失败,则唤醒当前节点的后继节点
           unparkSuccessor(node);
       }

       node.next = node; // help GC
   }
}


取消有三种状态


1. 当前节点是tail

2. 当前节点不是head的后继节点,也不是tail

3. 当前节点是head的后继节点


当前节点是tail,将该节点的前继节点的next指向null,也就是把当前节点移出队列 

深入理解AbstractQueuedSynchronizer-鸿蒙开发者社区

当前节点不是head的后继节点,也不是tail,这里将node的前继节点的next指向了node的后继节点,即compareAndSetNext(pred, predNext, next) 

深入理解AbstractQueuedSynchronizer-鸿蒙开发者社区

当前节点是head的后继节点,直接unpark后继节点 

深入理解AbstractQueuedSynchronizer-鸿蒙开发者社区

注意的是最后两种状态都会执行node.next = node,即将next指针指向自己,这关系着后面唤醒的时候是从尾部向前遍历,并且修改指针的这些操作都是CAS操作,并不保证成功


//唤醒后继节点
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);
}


这里要注意一点是从队尾向前遍历,不是从队首向后遍历,可以看一下cancelAcquire方法的处理过程,cancelAcquire只是设置了next的变化,没有设置prev的变化,在最后有这样一行node.next=node,如果这时执行了unparkSuccessor方法,并且向后遍历的话,就成了死循环了,所以这时只有prev是稳定的


接着来看解锁的过程


//调用ReentrantLock公平锁的unlock()方法其实就是调用release(1)
public final boolean release(int arg) {
   //尝试释放锁
   //成功了要唤醒后继节点的线程
   //这样其他线程才有机会执行
   if (tryRelease(arg)) {
       //释放成功后unpark后继节点的线程
       Node h = head;
       if (h != null && h.waitStatus != 0)
           unparkSuccessor(h);
       return true;
   }
   return false;
}


让子类去实现

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

后记

最后说一个用的特别巧的方法,即parkAndCheckInterrupt()方法


//自旋获取锁,直到获取锁成功,或者异常退出
//但是并不是busy acquire,因为当获取失败后会被挂起,由前驱节点释放锁时将其唤醒
//同时由于唤醒的时候可能有其他线程竞争,所以还需要进行尝试获取锁,体现的非公平锁的精髓。
final boolean acquireQueued(final Node node, int arg) {
   boolean failed = true;
   try {
       boolean interrupted = false;
       for (;;) {
           //获取前继节点
           final Node p = node.predecessor();
           //node节点的前继节点是head节点,尝试获取锁
           //如果成功说明head节点已经释放锁了
           //将node设为head开始运行
           if (p == head && tryAcquire(arg)) {
               setHead(node);
               p.next = null; // help GC
               failed = false;
               return interrupted;
           }
           //1.获取锁失败后是否可以挂起
           //2.如果可以挂起,则挂起当前线程(获取锁失败的节点)
           if (shouldParkAfterFailedAcquire(p, node) &&
               parkAndCheckInterrupt())
               interrupted = true;
       }
   } finally {
       if (failed)
           cancelAcquire(node);
   }
}


private final boolean parkAndCheckInterrupt() {
   LockSupport.park(this);
   return Thread.interrupted();
}


看acquireQueued这个方法,里面是一个死循环,但是并不会引起CPU使用率飙升,因为获取失败后会被挂起,这个判断是否中断的方法用的特别巧,先举2个例子


public static void main(String[] args) throws InterruptedException {
   Thread thread = new Thread();
   thread.start();
   Thread.sleep(1000);
   thread.interrupt();
   boolean flag1 = thread.isInterrupted();
   //false
   System.out.println(flag1);
   boolean flag2 = thread.isInterrupted();
   //false
   System.out.println(flag2);
}


public static void main(String[] args) {
   Thread.currentThread().interrupt();
   boolean flag1 = Thread.interrupted();
   //true
   System.out.println(flag1);
   boolean flag2 = Thread.interrupted();
   //false
   System.out.println(flag2);
}


这里先说一下isInterrupted和interrupted的方法的异同点,2个方法都能返回线程是否是中断状态,所不同的是isInterrupted不会清除这种状态,而interrupted则会清除这种这种状态(即中断状态的复位),所以两次调用interrupted,第一次为true,第二次则为false


为什么AbstractQueuedSynchronizer要用Thread.interrupted()这种方法来返回中断状态呢,我再写2个例子



public static void main(String[] args) {
   LockSupport.park();
   //end被一直阻塞没有输出
   System.out.println("end");
}


public static void main(String[] args) {
   Thread.currentThread().interrupt();
   LockSupport.park();
   //输出end
   System.out.println("end");
}


可以看到当线程被中断时,调用park()方法并不会被阻塞


public static void main(String[] args) {
   Thread.currentThread().interrupt();
   LockSupport.park();
   //返回中断状态,并且清除中断状态
   Thread.interrupted();
   //输出start
   System.out.println("start");
   LockSupport.park();
   //end被阻塞,没有输出
   System.out.println("end");
}


park与wait的作用类似,但是对中断状态的处理并不相同。如果当前线程不是中断的状态,park与wait的效果是一样的;如果一个线程是中断的状态,这时执行wait方法会报java.lang.IllegalMonitorStateException,而执行park时并不会报异常,而是直接返回。


到这我们就能理解为什么要进行中断的复位了


1. 如果当前线程是非中断状态,则在执行park时被阻塞,返回中断状态false

2. 如果当前线程是中断状态,则park方法不起作用,返回中断状态true,interrupted将中断复位,变为false

3. 再次执行循环的时候,前一步已经在线程的中断状态进行了复位,则再次调用park方法时会阻塞


所以这里要对中断进行复位,是为了不让循环一直执行,让当前线程进入阻塞状态,如果不进行复位,前一个线程在获取锁之后执行了很耗时的操作,那么岂不是要一直执行死循环,造成CPU使用率飙升




文章转载自公众号:Java识堂

分类
标签
已于2023-11-9 11:46:32修改
收藏
回复
举报
回复
    相关推荐