2.1、J.U.C和锁(上篇)
2.1.1、什么是AQS?
答:
AQS(AbstractQueuedSynchronizer)是多线程同步器,它是一个Java并发包中的一个类,它是实现同步器的基础框架。通过内置的FIFO队列(等待队列)和多个状态变量来实现同步。
AQS提供了两种锁机制:独占锁和共享锁。
- 独占锁:又叫排他锁,就是在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源。也就是说同一时刻多个线程中只能有一个线程获得锁资源。比如Lock中的ReentrantLock(重入锁)
- 共享锁:又叫读锁,就是在同一时刻允许多个线程同时获得锁资源。比如Semaphore、CountDownLatch
AQS的设计使得它非常灵活,可以通过继承AQS来实现各种不同的同步器。在Java并发包中,很多常用的同步器(如ReentrantLock、Semaphore、CountDownLatch等)都是基于AQS实现的。
扩展:
AQS的核心思想:使用一个int类型的volatile变量来表示同步状态,通过CAS操作来进行状态的获取和释放。当一个线程尝试获取锁时,如果当前同步状态为0,则可以获取锁,否则将当前线程加入等待队列,并进行自旋等待。当一个线程释放锁时,将唤醒等待队列中的下一个线程。
2.1.2、如何理解AQS的实现原理?
答:
- 等待队列:AQS内部维护了一个等待队列,用于存放等待获取锁资源的线程。当线程尝试获取锁但失败时,会被加入到等待队列中。这个队列是双向链表结构的。
- 状态管理:AQS使用一个整型变量(state)来表示共享的状态。状态的具体含义和用途由具体实现决定,比如ReentrantLock使用状态表示锁的持有次数。AQS内部提供了一些方法来操作状态,比如getState()获取当前状态的值,setState(int newState)设置新的状态值。
- CAS(Compare And Swap)操作:AQS使用CAS操作来对共享的状态进行更新。CAS操作主要有三个参数:内存位置V、旧的预期值A、即将更新的新值B。CAS操作会比较内存位置V的值和预期值A,若相等,则将内存位置V的值更新为新值B。CAS操作是原子的,当多个线程进行CAS操作时,只有一个线程会成功。
- 获取锁:当线程尝试获取锁时,会调用AQS的acquire方法。acquire方法会先尝试通过CAS操作来修改状态值,若成功则表示获取锁成功,若失败则表示获取锁失败。获取锁失败的线程会被加入到等待队列中,并挂起线程。
- 释放锁:当线程释放锁时,会调用AQS的release方法。release方法会先更新状态值,然后尝试唤醒等待队列中的线程,使它们有机会竞争获取锁。
扩展:
通过等待队列、状态管理和CAS操作实现了同步器(Synchronizer)的基本功能,AQS可用于实现各种同步工具,比如ReentrantLock、CountDownLatch、Semaphore等。
2.1.3、AQS为什么要使用双向链表?
答:
双向链表有两个指针,一个指针指向前置节点,另一个指针指向后继节点。所以,双向链表支持在O(1)时间复杂度下找到前置节点,所以在对线程进行插入和移除的时候,双向链表要比单向链表更简单、高效。
扩展:
- 没有竞争到锁的线程加入阻塞队列,并且阻塞等待的前提是当前线程所在节点的前置节点是正常状态,这是为了避免链表中存在异常线程导致无法唤醒后续线程。所以,线程阻塞之前需要判断前置节点的状态,如果AQS使用的是单向链表,当前线程所在节点就没有指针指向前置节点,就要从head节点开始遍历,性能非常低。
- 没有竞争到锁的线程加入到同步队列等待以后,是允许外部线程通过interrupt()方法触发唤醒并中断的,被中断的线程的状态会被修改为cancelled。被标记为cancelled状态的线程是不需要去竞争锁的,需要从双向链表中被移除。如果AQS使用的是单向链表,就需要从head节点开始往下逐个遍历,找到并移除异常状态的节点,效率很低。
总结就是:链表节点能够保存等待线程的信息,包括线程的等待状态、等待时间等信息,所以使用链表;双向链表结构可以很方便地支持节点的插入和删除操作,所以AQS使用双向链表。