AQS 是 AbstractQueuedSynchronizer 的简称,是并发编程中比较核心的组件。
在很多大厂的面试中,面试官对于并发编程的考核要求相对较高,简单来说,如果你不懂并发编程,那么你很难通过大厂高薪岗位的面试。
Hello,大家好,一个工作了 10 年的程序员,今天来和大家聊聊并发编程中的 AQS 组件。
我们来看一下,关于“谈谈你对 AQS 的理解“,看看普通人和高手是如何回答的!
AQS 全称是 AbstractQueuedSynchronizer,它是 J.U.C 包中 Lock 锁的底层实现,可以用它来实现多线程的同步器!
AQS 是多线程同步器,它是 J.U.C 包中多个组件的底层实现,如 Lock、CountDownLatch、Semaphore 等都用到了 AQS.
从本质上来说,AQS 提供了两种锁机制,分别是排它锁,和共享锁。
排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源,比如 Lock 中的ReentrantLock 重入锁实现就是用到了 AQS 中的排它锁功能。
共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如CountDownLatch 和 Semaphore 都是用到了 AQS 中的共享锁功能。
锁或者其他同步组件一般都会定义一个静态内部类,该静态内部类会继承AQS,同时重写AQS中的方法,重写AQS中的方法时需要用到下面三个方法来获取同步状态。
总结:如何自定义一个锁或者同步组件?
创建静态内部类继承AQS,重写AQS中的可重写的方法,在里面使用AQS提供的如上三个方法来获取、修改同步状态。最后调用AQS中的模板方法来进行操作,模板方法中会调用重写的方法。
即使用者调用模板方法,模板方法调用重写方法,重写方法调用如上三个方法。
protected boolean tryAcquire(int arg);
独占式获取同步状态,查询当前状态并根据具体条件设置同步状态。
protected boolean tryRelease(int arg);
独占式释放同步状态,等待的线程有机会获取同步状态。
protected int tryAcquireShared(int arg);
共享式获取同步状态,返回大于等于0的值表示获取成功,反之获取失败。
4.
protected boolean tryReleaseShared(int arg);
共享式释放同步状态。
5.
protected boolean isHeldExclusively();
表示是否被当前线程占用。
当前线程获取成功则会返回,否则进入同步队列等待,调用重写方法中的tryAcquire。
void acquireInterruptible(int arg);
如果当前线程被中断,则会抛出InterruptedException。
boolean tryAcquireNanos(int arg, long nanos);
在acquireInterruptible的基础上设置超时时间,如果超时时间还没有获取到同步状态,会返回false,否则返回true。 4. 共享获取同步状态
void acquireShared(int arg);
void acquireSharedInterruptible(int arg);
boolean tryAcquireSharedNanos(int arg, long nanos);
boolean release(int arg);
同步队列中的第一个节点将会被唤醒。 8. 共享式释放同步状态
boolean releaseShared(int arg);
Collection<Thread> getQueuedThreads();
总之:模板可以分为三类:独占式获取与释放同步状态、共享式获取与释放同步状态、查询同步队列线程等待情况。获取又有分为中断、超时。
public class UnReetrantLock implements Lock {
public static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0);
return false;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
public Condition newCondition() {
return new ConditionObject();
}
}
private 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();
}
}
底层数据结构:同步队列
AQS中使用一个双向链表来保存等待同步状态的线程,链表的节点用其内部自定义的Node表示,Node类源码:
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}
waitStatus有五个状态:
同步队列采用尾插法的方式,同时会使用CAS保证尾插的时候是线程安全的。其结构如下:
其中队头是获取同步状态成功的节点,当首节点的线程释放同步状态的时候,会唤醒后继的节点,后继节点会成为首节点。(这个过程不用CAS,没有竞争的情况。)
acquire方法流程
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();
}
同步队列中的节点不断地在自旋判断其前驱节点是不是头节点,如果是则尝试获取同步状态,否则会阻塞节点中的线程。
acquireShared方法流程
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) doAcquireShared(arg);
}
ReentrantLock,支持重入锁和公平与非公平锁。
重入锁:支持线程反复地获取锁资源而不会自己阻塞自己,有两个问题要实现:
例如非公平锁每次再尝试获取锁的时候都会判断是不是同个线程,如果是的话增加计数器的值。释放锁时等到计数器的值为0时才将占有锁的线程设置为null。
公平锁:获取锁的线程按照绝对的时间顺序,FIFO。
非公平锁:只要CAS设置同步状态成功,就获取锁,不会按照FIFO顺序。
ReentrantLock的构造方法中传入true时可以创建公平锁:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁在tryAcquire的时候会判断当前线程是否有前驱节点,有的话则会等待前驱节点释放之后在获取尝试获取锁。 公平锁的tryAcquire:
hasQueuePredecessors方法用来判断是否有前驱节点
非公平锁的tryAcquire:
问:如何实现公平锁? 构造函数的参数传入true,在重写的tryAcquire方法中判断当前线程是否有前驱线程,有的话尝试获取同步状态失败,以此来达到公平的效果。
对比: 公平锁虽然会按照FIFO原则,但是会进行大量的线程切换,非公平锁虽然可能会造成其他线程饥饿,但是可以极大提高吞吐量。