深入理解AQS


1. AQS

1.1 AQS 是什么

AQS是 AbstractQueuedSynchronizer 的简称,即抽象队列同步器 。是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量state表示持有锁的状态。

数据结构

在这里插入图片描述

它并不是直接储存线程,而是储存拥有线程的Node结点

// Node 部分源代码
static final class Node {
 // 标记⼀个结点(对应的线程)在共享模式下等待
 static final Node SHARED = new Node();
 // 标记⼀个结点(对应的线程)在独占模式下等待
 static final Node EXCLUSIVE = null; 
 // waitStatus的值,表示该结点(对应的线程)已被取消
 static final int CANCELLED = 1; 
 // waitStatus的值,表示后继结点(对应的线程)需要被唤醒
 static final int SIGNAL = -1;
 // waitStatus的值,表示该结点(对应的线程)在等待某⼀条件
 static final int CONDITION = -2;
 // waitStatus的值,表示有资源可⽤,新head结点需要继续唤醒后继结点(共享模式下)
 static final int PROPAGATE = -3;
 // 等待状态,取值范围,-3,-2,-1,0,1
 volatile int waitStatus;
 volatile Node prev; // 前驱结点
 volatile Node next; // 后继结点
 volatile Thread thread; // 结点对应的线程
 Node nextWaiter; // 等待队列⾥下⼀个等待条件的结点
 // 判断共享模式的⽅法
 final boolean isShared() {
     return nextWaiter == SHARED;
 }
 Node(Thread thread, Node mode) { // Used by addWaiter
     this.nextWaiter = mode;
     this.thread = thread;
 }
 // 其它⽅法忽略,可以参考具体的源码 
}
// AQS⾥⾯的addWaiter私有⽅法
private Node addWaiter(Node mode) {
 // 使⽤了Node的这个构造函数
 Node node = new Node(Thread.currentThread(), mode);
 // 其它代码省略 
 }

资源有两种共享模式,或者说两种同步⽅式:

  1. 独占模式(Exclusive):资源是独占的,⼀次只能⼀个线程获取。如 ReentrantLock。
  2. 共享模式(Share):同时可以被多个线程获取,具体的资源个数可以通过参数指定。如Semaphore/CountDownLatch。

1.2 AQS 主要方法源码解析

AQS的设计是基于模板方法模式的,它有一些方法必须要去子类去实现的,它们主要有:

  • isHeldExclusively():该线程是否正在独占资源。只有⽤到condition才需要去实现它。
  • tryAcquire(int):独占⽅式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占⽅式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享⽅式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可⽤资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享⽅式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

这些⽅法虽然都是 protected ⽅法,但是它们并没有在AQS具体实现,⽽是直接抛出异常:

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

⽽AQS实现了⼀系列主要的逻辑。下面从源码来分析⼀下获取和释放资源的主要逻辑。

1.2.1 获取资源

获取资源的入口是acquire(int arg)⽅法。arg是要获取的资源的个数,在独占模式下始终为1。先来看看这个⽅法的逻辑:

  public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

⾸先调⽤tryAcquire(arg)尝试去获取资源。前⾯提到了这个⽅法是在⼦类具体实现的。

如果获取资源失败,就通过addWaiter(Node.EXCLUSIVE)⽅法把这个线程插入到等待队列中。其中传入的参数代表要插入的Node是独占式的。这个⽅法的具体实现:

private Node addWaiter(Node mode) {
 // ⽣成该线程对应的Node结点
 Node node = new Node(Thread.currentThread(), mode);
 // 将Node插⼊队列中
 Node pred = tail;
 if (pred != null) {
 	node.prev = pred;
 	// 使⽤CAS尝试,如果成功就返回
     if (compareAndSetTail(pred, node)) {
         pred.next = node;
         return node;
     }
 }
 // 如果等待队列为空或者上述CAS失败,再⾃旋CAS插⼊
 enq(node);
 return node;
}
// ⾃旋CAS插⼊等待队列
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;
         }
 	 }
  }
}

现在回到最开始的aquire(int arg)⽅法。现在通过addWaiter⽅法,已经把⼀个Node放到等待队列尾部了。而处于等待队列的结点是从头结点⼀个⼀个去获取资源的。具体的实现我们来看看acquireQueued方法

   final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋
            for (;;) {
                final Node p = node.predecessor();
                // 如果node的前驱结点p是head,表示node是第二个结点,就可以尝试去获取资源了
                // 因为head结点是一个虚结点(哨兵结点)起占位作用
                if (p == head && tryAcquire(arg)) {
                    // 拿到资源后,将head指向该结点。
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 获取锁失败后, 判断是否把当前线程挂起  这才是线程真正入队阻塞等待
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这⾥parkAndCheckInterrupt⽅法内部使⽤到了LockSupport.park(this),顺便简单介绍⼀下park。

LockSupport类是Java 6 引⼊的⼀个类,提供了基本的线程同步原语。

LockSupport实际上是调⽤了Unsafe类⾥的函数,归结到Unsafe⾥,只有两 个函数:

  • park(boolean isAbsolute, long time):阻塞当前线程
  • unpark(Thread jthread):使给定的线程停⽌阻塞

LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认是0。

所以结点进⼊等待队列后,是调⽤park使它进入阻塞状态的。只有头结点的线程是处于活跃状态的。

1.2.2 释放资源

释放资源相⽐于获取资源来说,会简单许多。在AQS中只有⼀⼩段实现。源码:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
   		Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
            return true;
        }
    return false;
}
    
 private void unparkSuccessor(Node node) {
    // 如果状态是负数,尝试把它设置为0 
     int ws = node.waitStatus;
    if (ws < 0)
    compareAndSetWaitStatus(node, ws, 0);
    // 得到头结点的后继结点head.next
     Node s = node.next;
     // 如果这个后继结点为空或者状态⼤于0
     // 通过前⾯的定义我们知道,⼤于0只有⼀种可能,就是这个结点已被取消 
    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);
}

1.3 基于AQS 实现的锁

  1. Semaphore 信号量 :可以指定多个线程同时访问某个资源。

    public class SemaphoreDemo {
    
        public static void main(String[] args) {
            /**
             * 模拟资源类,有3个空车位
             */
            Semaphore semaphore = new Semaphore(3);
            for (int i = 0; i <6 ; i++) {
              new Thread(()->{
                 try {
                   semaphore.acquire();
                   System.out.println(Thread.currentThread().getName()+"\t"+"抢占到了车位");
                   TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"\t"+"离开了车位");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        semaphore.release();
                    }
                },String.valueOf(i)).start();
            }
        }
    }
    
    • acquire() 方法 获取一个许可证,然后对共享资源进行操作;如果许可分配完了,那么线程阻塞等待,直到其他线程释放许可才有机会获取到。
    • release()方法 线程释放一个许可。
  2. CountDownLatch (倒计时器):是⼀个同步⼯具类,⽤来协调多个线程之间的同步。这个⼯具通常⽤来控制线程等待,它可以让某⼀个线程等待直到倒计时结束,再开始执⾏。

    public class CountDownLatchDemo {
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch countDownLatch = new CountDownLatch(6);
            for (int i = 0; i <6 ; i++) {
                new Thread(()->{
                    System.out.println(Thread.currentThread().getName()+"\t"+"离开教室");
                    countDownLatch.countDown();
                },String.valueOf(i)).start();
            }
            countDownLatch.await();
            System.out.println(Thread.currentThread().getName()+"\t"+"同学们都走了,班长关闭教室门。");
        }
    }    
    
    • await()方法会让线程阻塞,它会等待直到count的值为0才会继续执行。

    • countDown()方法将count值减1

  3. CyclicBarrier(循环栅栏)类似于CountDownLatch,它能阻塞一组线程直到某个事件的发送。

    # 七龙珠
    public class CycliBarrierDemo {
        public static void main(String[] args) {
            CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
                System.out.println("****收集成功,合成七龙珠");
            });
            for (int i = 1; i <=7 ; i++) {
                final  int t = i;
                new Thread(()->{
                    System.out.println(Thread.currentThread().getName()+"\t收集到第 :"+t+"颗龙珠");
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                },String.valueOf(i)).start();
            }
        }
    }
    
  4. ReentrantReadWriteLock(读写锁),它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写操作相关的锁,称为排它锁。

    class MyCache {
    
        private volatile Map<String, Object> map = new HashMap<>();
        private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        
        public void put(String s, Object o) {
            readWriteLock.writeLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t ----写入数据");
                map.put(s, o);
                System.out.println(Thread.currentThread().getName() + "\t ---写入完成");
            } finally {
                readWriteLock.writeLock().unlock();
            }
        }
    
        public void get(String s) {
            readWriteLock.readLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t 读取数据");
                Object result = map.get(s);
                System.out.println(Thread.currentThread().getName() + "\t 读取完成  " + result);
            } finally {
                readWriteLock.readLock().unlock();
            }
        }
    }
    public class ReadWriteLockDemo {
    
        public static void main(String[] args) {
            MyCache myCache = new MyCache();
            for (int i = 0; i <5 ; i++) {
                final int t = i;
                new Thread(()->{
                    myCache.put(String.valueOf(t),t);
                },String.valueOf(i)).start();
            }
            /**
             * 5个线程可以同时对key为"0"的进行数据读取
             */
            for (int i = 0; i <5 ; i++) {
                new Thread(()->{
                    myCache.get(String.valueOf(0));
                },String.valueOf(i)).start();
            }
    
        }
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值