【JUC】1、Java AbstractQueuedSynchronizer解析与面试题(一)

1、带着面试题看源码

带着问题看书(源码)是个好习惯~, 先看看这些问题,最后再做解答。

  1. 什么是AQS? 为什么它是核心?
  2. AQS的核心思想是什么? 它是怎么实现的? 底层数据结构等
  3. AQS有哪些核心的方法?
  4. AQS定义什么样的资源获取方式?
  5. AQS底层使用了什么样的设计模式?
  6. AQS的应用示例?

2、理论

2.1 、简介

AQS提供了一个用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关的同步器(信号量,事件等)的框架。此类旨在为大多数依赖单个原子{@code int}值表示状态的同步器提供有用的基础。

默认互斥模式和共享模式之一或两者。

整个框架的关键就是如何管理被阻塞的线程的队列,该队列是严格的FIFO队列,因此,框架不支持基于优先级的同步。

此类是ReentrantLock、Semaphore等的基础实现,很重要!!!

2.2、设计

总体来说,使用变种的CLH实现

2.2.1 CLH介绍

CLH是一种基于单向链表的高性能、公平的自旋锁。
基于当前节点的前驱节点状态进行自旋(前驱节点如果处于加锁状态或等待状态,当前节点自旋;前驱节点未加锁状态,当前节点得到锁)
前驱节点解锁后,当前节点会结束自旋,并进行加锁。

CLH介绍与Java实现 https://blog.csdn.net/hhy107107/article/details/108043485

2.2.2 与标准的CLH的区别:

  1. 为了将CLH队列用于阻塞式同步器,需要做些额外的修改以提供一种高效的方式定位某个节点的后继节点。在自旋锁中,一个节点只需要改变其状态,下一次自旋中其后继节点就能注意到这个改变,所以节点间的链接并不是必须的。但在阻塞式同步器中,一个节点需要显式地唤醒(unpark)其后继节点。

AQS队列的节点包含一个next链接到它的后继节点。但是,由于没有针对双向链表节点的类似compareAndSet的原子性无锁插入指令,因此这个next链接的设置并非作为原子性插入操作的一部分,而仅是在节点被插入后简单地赋值:

pred.next = node;

next链接仅是一种优化。如果通过某个节点的next字段发现其后继结点不存在(或看似被取消了),总是可以使用pred字段从尾部开始向前遍历来检查是否真的有后续节点。

  1. 对CLH队列主要的修改是将每个节点都有的状态字段用于控制阻塞而非自旋

在同步器框架中,仅在线程调用具体子类中的tryAcquire方法返回true时,队列中的线程才能从acquire操作中返回;而单个“released”位是不够的。但仍然需要做些控制以确保当一个活动的线程位于队列头部时,仅允许其调用tryAcquire;这时的acquire可能会失败,然后(重新)阻塞。这种情况不需要读取状态标识,因为可以通过检查当前节点的前驱是否为head来确定权限。与自旋锁不同,读取head以保证复制时不会有太多的内存竞争( there is not enough memory contention reading head to warrant replication.)。然而,“取消”状态必须存在于状态字段中。

队列节点的状态字段也用于避免没有必要的park和unpark调用。虽然这些方法跟阻塞原语一样快,但在跨越Java和JVM runtime以及操作系统边界时仍有可避免的开销。在调用park前,线程设置一个“唤醒(signal me)”位,然后再一次检查同步和节点状态。一个释放的线程会清空其自身状态。这样线程就不必频繁地尝试阻塞,特别是在锁相关的类中,这样会浪费时间等待下一个符合条件的线程去申请锁,从而加剧其它竞争的影响。除非后继节点设置了“唤醒”位(译者注:源码中为-1),否则这也可避免正在release的线程去判断其后继节点。这反过来也消除了这些情形:除非“唤醒”与“取消”同时发生,否则必须遍历多个节点来处理一个似乎为null的next字段。

同步框架中使用的CLH锁的变体与其他语言中的相比,主要区别可能是同步框架中使用的CLH锁需要依赖垃圾回收管理节点的内存,这就避免了一些复杂性和开销。但是,即使依赖GC也仍然需要在确定链接字段不再需要时将其置为null。这往往可以与出队操作一起完成。否则,无用的节点仍然可触及,它们就没法被回收。

其它一些更深入的微调,包括CLH队列首次遇到竞争时才需要的初始空节点的延迟初始化等,都可以在J2SE1.5的版本的源代码文档中找到相应的描述。

3、实践(基于AQS的互斥锁实现与队列图解)

3.1、基于AQS的互斥锁实现

public class Mutex implements Lock {

    private static 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 (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

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

    private final Sync sync = new Sync();

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

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @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();
    }
}
public class MutexTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 4, 1,
                TimeUnit.HOURS, new ArrayBlockingQueue<Runnable>(10));
        Mutex m = new Mutex();
        m.lock();
        System.out.println("nnn1");
        threadPoolExecutor.submit(() -> {
            System.out.println("nnn2");
            m.lock();
            System.out.println("A");
        });
        Thread.sleep(1000L);
        System.out.println("nnn3");
        m.unlock();
    }
}

3.2 图解代码

下图展示了上面测试代码m.lock 、 m.lock 、 m.unlock 分别执行后,AQS队列中节点的变化。有助于理解AQS
在这里插入图片描述

4、面试题解答

  1. 什么是AQS? 为什么它是核心?
    a)AQS是用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关的同步器。
    b)为什么是核心:因为CountDownLatch、Semaphore、ReentrantLock都是依赖AQS实现的
  2. AQS的核心思想是什么? 它是怎么实现的? 底层数据结构等
    a) 核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制。
    b)怎么实现的:一个volatile 的int类型标志(state)表示共享资源,锁的算法是CLH。将获取不到锁的线程封装成节点放到队列中。
  3. AQS有哪些核心的方法?
    acquire、release
  4. AQS定义什么样的资源获取方式?
    互斥(acquire/ release)、共享(acquireShared/releaseShared)
  5. AQS底层使用了什么样的设计模式?
    模板方法模式:

定义:定义一个操作中的算法的框架,将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
https://blog.csdn.net/hhy107107/article/details/107731784

AQS定义好了一套模板方法,子类去实现AQS的时候,只要重写AQS提供的方法,就可以实现不同的需求。以acquire为例子,aqs中排他地获取资源使用acquire方法,在acquire方法里面调用了 tryAcquire方法。AQS没有实现tryAcquire,需要子类根据自己的需求去实现。

  1. AQS的应用示例?
    CountDownLatch、Semaphore、ReentrantLock

5、AQS源码注释部分翻译

https://blog.csdn.net/hhy107107/article/details/108043686


参考:
http://ifeve.com/aqs/
https://segmentfault.com/a/1190000016885682?utm_source=tag-newest

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值