AQS详解与源码分析

AQS详解与源码分析

AQS介绍

  • AQS全称为AbstractQueuedSynchronizer,这个类在java.util.concurrent.locks包下,简称同步器
  • 同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成等待队列
  • FIFO:一个带头结点的双向链式队列
  • ReentrantLock、Semaphore、ReentrantReadWriteLock、SynchronousQueue、FutureTask等都是基于AQS
    在这里插入图片描述

AQS原理分析

AQS原理概述
  • AQS核心思想
    • 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态
    • 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制
      • 这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中
  • CLH队列
    • 是一个虚拟的双向队列,即不存在队列实例,仅存在结点之间的关联关系
    • AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点来实现锁的分配
AQS的使用
  • 它的子类必须重写AQS的几个protected修饰的用来改变同步状态的方法,其他方法主要是实现了排队和阻塞机制。状态的更新使用getState()、setState()以及compareAndSetState()这三个方法
  • 子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以方便的实现不同类型的同步组件
AQS与锁的关系
  • 同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义
  • 锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作
  • 锁和同步器很好的隔离了使用者和实现者所需关注的领域
AQS对资源的共享方式
  • 两种资源共享方式
    • Exclusive(独占):只用一个线程能执行(ReentrantLock)
      • 公平锁:按照线程在队列中的排队顺序
      • 非公平锁:当线程要获取锁时,无视队列顺序直接抢锁
    • Share(共享):多个线程可同时执行(Semaphore、CountDownLatch、CyclicBarrier、ReadWriteLock)

模板方法设计模式

  • 模版方法模式的本质:固定算法骨架
  • 模板方法模式主要是通过制定模板,把算法步骤固定下来,至于谁来实现,模板可以自己提供实现,也可以由子类去实现,还可以通过回调机制让其他类来实现
  • 通过固定算法骨架来约束子类的行为,并在特定的扩展点来让子类进行功能扩展,从而让程序既有很好的复用性,又有较好的扩展性
AQS底层使用模板方法模式
  • 同步器将一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法
  • 总结
    • 同步组件(这里不仅仅指锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内存类;
    • AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法;
    • AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义;
    • 在重写AQS的方式时,使用AQS提供的getState()、setState()、compareAndSetState()方法进行修改同步状态
模板方法模式自定义同步器时需要重写的模板方法
isHeldExclusively()	//该线程是否正在独占资源。只有用到condition才需要去实现它。

tryAcquire(int)	//独占方式。尝试获取资源,成功则返回true,失败则返回false。

tryRelease(int)	//独占方式。尝试释放资源,成功则返回true,失败则返回false。

tryAcquireShared(int)	//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

tryReleaseShared(int)	//共享方式。尝试释放资源,成功则返回true,失败则返回false。
模板方法的分类
  1. 独占式获取与释放同步状态
  2. 共享式获取与释放同步状态
  3. 查询同步队列中等待线程情况
自定义同步组件
  • 在同步组件的实现上主要是利用了AQS,而AQS“屏蔽”了同步状态的修改,线程排队等底层实现,通过AQS的模板方法可以很方便的给同步组件的实现者进行调用
  • 针对用户来说,只需要调用同步组件提供的方法来实现并发编程即可
  • 关键点
    • 实现同步组件时推荐定义继承AQS的静态内存类,并重写需要的protected修饰的方法
    • 同步组件语义的实现依赖于AQS的模板方法,而AQS模板方法又依赖于被AQS的子类所重写的方法

AQS核心

  • 同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现,而这些实际上则是AQS提供出来的模板方法
同步队列
  • 当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列
  • AQS中的同步队列是通过带头结点的链式存储结构进行实现
  • 节点的数据结构,即AQS的静态内部类Node,节点的等待状态等信息
  • 同步队列是一个带头结点的双向队列,AQS通过持有头尾指针管理同步队列

独占锁

独占锁的获取(acquire()方法)
public final void acquire(int arg) {
    //先看同步状态是否获取成功,如果成功则方法结束返回
    //若失败则先调用addWaiter()方法再调用acquireQueued()方法
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
private Node addWaiter(Node mode) {
    // 1. 将当前线程构建成Node类型
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // 2. 当前尾节点是否为null
    Node pred = tail;
    if (pred != null) {
        // 2.2 将当前节点尾插入的方式插入同步队列中
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 2.1. 当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程
    enq(node);
    return node;
}
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            //1. 构造头结点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 2. 尾插入,CAS操作失败自旋尝试
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 1. 获得当前节点的先驱节点
            final Node p = node.predecessor();
            // 2. 当前节点能否获取独占式锁					
            // 2.1 如果当前节点的先驱节点是头结点并且成功获取同步状态,即可以获得独占式锁
            if (p == head && tryAcquire(arg)) {
                //队列头指针用指向当前节点
                setHead(node);
                //释放前驱节点
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 2.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;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
private final boolean parkAndCheckInterrupt() {
    //使得该线程阻塞
	LockSupport.park(this);
    return Thread.interrupted();
}
  • lock()方法会调用AQS的acquire()方法
    • 先调用tryAcquire()方法获取同步状态
    • 获取同步状态失败,调用addWaiter()和acquireQueued()方法执行入队操作
      • addWaiter()方法将当前线程构建成Node类型
        • 当前同步队列的尾节点为null,调用方法enq()插入,在当前线程是第一个加入同步队列时,调用方法完成链式队列的头结点的初始化
        • 当前队列的尾节点不为null,则采用尾插入的方式入队,在尾插入失败后,enq()方法会自旋不断尝试CAS尾插入节点直至成功为止(CAS保证线程安全)
      • acquireQueued()方法排队获取锁
        • 如果先驱节点是头结点的并且成功获得同步状态的时候(if (p == head && tryAcquire(arg))),当前线程能够获得锁,该方法执行结束退出
        • 获取锁失败的话,先使用shouldParkAfterFailedAcquire()方法将节点状态设置成SIGNAL,然后调用parkAndCheckInterrupt()方法中的LookSupport.park()方法使得当前线程阻塞
    • 获取锁成功,出队操作
      • 将当前节点通过setHead()方法设置为队列的头结点,然后将之前的头结点的next域设置为null并且pre域也为null
      • 即与队列断开,无任何引用方便GC时能够将内存进行回收
        在这里插入图片描述
独占锁的释放(release()方法)
public final void acquire(int arg) {
    //先看同步状态是否获取成功,如果成功则方法结束返回
    //若失败则先调用addWaiter()方法再调用acquireQueued()方法
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
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;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
		//后继节点不为null时唤醒该线程
        LockSupport.unpark(s.thread);
}
  • 如果同步状态释放成功(tryRelease返回true)会调用unparkSuccessor()方法
    • 首先获取头节点的后继节点,当后继节点的时候会调用LookSupport.unpark()方法java
    • 该方法会唤醒该节点的后继节点所包装的线程
      • 每一次锁释放后就会唤醒队列中该节点的后继节点所引用的线程,从而进一步可以佐证获得锁的过程是一个FIFO(先进先出)的过程
独占锁总结
  • 线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试
  • 线程获取锁是一个自旋的过程,当且仅当当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞
  • 释放锁的时候会唤醒后继节点
  • 在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点

独占锁特性学习

可中断式获取锁(acquireInterruptibly()方法)
public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
		//线程获取锁失败
        doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
	//将节点插入到同步队列中
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            //获取锁出队
			if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
				//线程中断抛异常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  • 在获取同步状态失败后会调用doAcquireInterruptibly()方法,与acquire方法逻辑几乎一致
  • 当parkAndCheckInterrupt()返回true时,即线程阻塞时该线程被中断java,代码抛出被中断异常
超时等待式获取锁(tryAcquireNanos()方法)
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
		//实现超时等待的效果
        doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
	//1. 根据超时时间和当前时间计算出截止时间
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
			//2. 当前线程获得锁出队列
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
			// 3.1 重新计算超时时间
            nanosTimeout = deadline - System.nanoTime();
            // 3.2 已经超时返回false
			if (nanosTimeout <= 0L)
                return false;
			// 3.3 线程阻塞等待 
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            // 3.4 线程被中断抛出被中断异常
			if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  • 通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果,该方法会在三种情况下才会返回
    1. 在超时时间内,当前线程成功获取了锁
    2. 当前线程在超时时间内被中断java
    3. 超时时间结束,仍未获得锁返回falsejava
  • 程序逻辑同独占锁可响应中断式获取基本一致,唯一的不同在于获取锁失败后
  • doAcquireNanos()方法实现超时等待的效果
    • 先计算出按照现在时间和超时时间计算出理论上的截止时间
    • 判断是否已经超时
      • 若超时,If判断之间返回false
      • 若未超时,if判断为true时就会执行LockSupport.parkNanos使得当前线程阻塞
        • 同时在增加对中断的检测,若检测出被中断直接抛出被中断异常
          在这里插入图片描述

共享锁

共享锁的获取(acquireShared()方法)
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);
                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()方法,tryAcquireShared()返回值是一个int类型,当返回值为大于等于0的时候方法结束说明获得成功获取锁
  • 否则,表明获取同步状态失败即所引用的线程获取锁失败,会执行doAcquireShared()方法
    • 自旋过程中能够退出的条件是当前节点的前驱节点是头结点并且tryAcquireShared(arg)返回值大于等于0,即能成功获得同步状态
共享锁的释放(releaseShared()方法)
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}
  • 调用tryReleaseShared()方法后会继续执行doReleaseShared方法
  • 在共享式锁的释放过程中,对于能够支持多个线程同时访问的并发组件,必须保证多个线程能够安全的释放同步状态
  • 当CAS操作失败则continue,在下一次循环中进行重试
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值