JVMJava多线程

J.U.C之AQS:大话AQS详解和使用

2019-04-30  本文已影响0人  贪睡的企鹅

J.U.C之AQS:AQS详解和使用

AQS是什么

AQS是AbstractQueuedSynchronizer的缩写,翻译过来就是"同步器",AbstractQueuedSynchronizer是一个抽象类,它实现了Java函数中锁同步(synchronized)锁等待(wait,notify)功能。

Java并包里大部分并发工具类都将其作为核心基础构件,比如可重入锁ReentrantLock, 信号量Semaphore基于各自的特点来使用AQS提供的基础能力方法实现多线程交互。

AQS核心功能

锁同步(synchronized)

锁等待(wait,notify)

AQS 中概念

同步状态

AQS实现了锁,必然需要一个竞争对象。AQS存在从一个int类型的成员变量state,我们把它称为同步状态,基于开闭原则,内部提供了很多模板方法【参考AQS核心方法】给子类去定制如何获取释放同步状态。

AQS按照获取释放同步状态的方式分为"独占式同步","共享式同步"。

独占式同步

从概念上来说独占式对应只存在一个资源,且只能被一个线程或者说竞争者占用.

共享式同步

从概念上来共享式对应存在多个资源的是有多个线程或者竞争者能够获取占用。他们对应的场景不同因而流程上会有差异

同步队列

AQS 实现了锁那么总需要一个队列将无法获取锁的线程保存起来,方便在锁释放时通知队列中线程去重新竞争锁。

同步队列又被称为CLH同步队列,CLH队列是通过链式方式实现FIFO双向队列。当线程获取同步状态失败时,AQS则会将当前线程构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态被释放时,会把首节点后第一个节点的线程从阻塞状态下唤醒,唤醒的线程会尝试竞争同步状态,如果获取同步状态成功,则从同步队列中出队,如果获取同步状态失败则继续阻塞。

同步队列.png
栗子
public class LockDemo {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                lock.lock();
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            });
            thread.start();
        }
    }
}

实例代码中开启了5个线程,先获取锁之后再睡眠10S中。通过debug,当Thread-4(在本例中最后一个线程)获取锁失败后进入同步时,AQS时现在的同步队列如图所示

同步栗子.png

条件队列

Java 传统的监视器有如下函数 wait、notify、notifyAll。

它们可以实现当一个线程获取锁时,它可以主动放弃(wait)锁进入阻塞状态,同时被添加进入一个条件队列中。只有其他线程通知唤醒(notify/notifyAll)时才从条件队列中出队,并尝试获取锁,并在获取锁成功后继续执行之前的未完成代码逻辑。

AQS内部存在一个内部类实现了Condition接口, 在AQS内部维护着一条链表实现单向条件队列。使用AQS获取内部实现Condition接口对象,调用await(),signal(),signalAll()函数实现Java中wait、notify、notifyAll同样功能。

条件队列.png
栗子
public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(() -> {
            lock.lock();
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });
        thread.start();
    }
}

新建了10个线程,每个线程先获取锁,然后调用condition.await方法释放锁将当前线程加入到等待队列中,通过debug控制当走到第10个线程的时候查看firstWaiter即等待队列中的头结点,debug模式下情景图如下:

条件队列栗子.png

AQS 实现原理

AQS核心是一个同步状态,两个队列。它们实现了Java函数中锁同步(synchronized),锁等待(wait,notify),并在其基础上实现了独占式同步,共享式同步2中方式锁的实现。

同步队列.png 条件队列.png

AQS核心方法

独占式同步

acquire

获取同步状态。如果当前线程获取同步状态成功则直接返回,如果获取失败插入同步队列尾部,同时线程阻塞。当调用release释放同步状态时,会从同步队列head头部后第一个节点中线程从阻塞中释放并在自旋中重新竞争同步状态,如果获取成功则从同步队列出队,并返回,如果获取失败则继续阻塞,等待下次唤醒。

acquireInterruptibly

独占式获取同步状态,与acquire方法相同。但在如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;

tryAcquireNanos

独占式获取同步状态,与acquireInterruptibly方法相同。但在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false

release

释放独占式同步状态,唤醒同步队列中首节点之后的第一个等待节点的线程的阻塞。

共享式同步

acquireShared

获取同步状态,如果当前线程获取同步状态成功则直接返回。如果获取失败插入同步队列尾部,同时线程阻塞。当调用releaseShared释放同步状态时,会找到从head头部节点后置节点中的线程,并将该线程从阻塞中释放。被释放的线程会在自旋中重新竞争同步状态。如果获取成功则出队,同时会释放后置节点中的线程从阻塞中唤醒竞争同步状态。

acquireSharedInterruptibly

在acquireShared方法基础上增加了能响应中断的功能;

tryAcquireSharedNanos

在acquireSharedInterruptibly基础上增加了超时等待的功能;

releaseShared

释放共享式同步状态,释放共享式同步状态会唤醒同步队列中首节点之后的第一个等待节点的线程的阻塞。

同步状态

getState()

返回同步状态的当前值

setState(int newState)

设置当前同步状态

compareAndSetState(int expect, int update)

使用CAS设置当前状态,该方法能够保证状态设置的原子性

同步队列

hasQueuedThreads()

查询是否有任何线程正在等待获取。【在同步队列中是否存在等待线程】

int getQueueLength()

返回等待获取的线程数的估计值.在同步队列中是否存在等待线程数量】

getQueuedThreads()

返回包含可能等待获取的线程的集合。因为实际的线程集可能在构造此结果时动态地改变,所以返回的集合仅是尽力而为的估计值【返回同步队列中线程集合】

AQS模板方法

我们可以编写自己类继承AQS选择重写独占式或共享式模板方法,从而定义如何获取同步状态和释放同步状态的逻辑。

独占式

tryAcquire

尝试独占式获取同步状态,返回值为true则获得同步状态成功,否则获取失败。

调用场景:

tryRelease

尝试独占式释放同步状态,返回值为true则表示获取成功,否则获取失败。

调用场景:

共享式

tryAcquireShared

尝试共享式获取同步状态,当返回值为大于等于0的时获得同步状态成功,否则获取失败。

调用场景:

(该线程从阻塞中释放,会在自旋中重新竞争同步状态)

tryReleaseShared

尝试共享式释放同步状态,返回值为true则表示获取成功,否则获取失败。

调用场景:

独占式 VS 共享式

从概念上来说独占式对应只存在一个资源,且只能被一个线程或者说竞争者占用,而共享式对应存在多个资源的是有多个线程或者竞争者能够获取占用。他们对应的场景不同因而流程上会有差异。

流程区别

在流程上来看只有加粗的部分是共享式所独有的:

尝试获取同步失败 --> 进入等待队列排队 --> 阻塞当前线程 --> 当等待队列排到自己被唤醒 --> 尝试获取锁(可能被其他线程插队而导致获取锁失败,失败在次阻塞,等待下次排到自己)--> 尝试获取锁成功,通知等待队列前面共享节点线程从阻塞中唤醒 --> 执行自己的业务逻辑 --> 尝试释放锁--> 成功通知等待队列前面共享节点线程从阻塞中唤醒。

独占式

对于独占式,当节点中线程从阻塞中释放,获取同步状态成功后,就开始执行,直到完成,释放同步状态才会通知等待队列节点线程从阻塞中唤醒,由于独占式通常只有一个资源。因而就没有必要在获取锁成功后通知同步队列线程去尝试获取同步状态,因为自己还没做完呢,也就符合同时只能一人执行特性。只有自己执行完后才通知同步队列线程获取同步状态。

共享式

对于共享式,当节点中线程从阻塞中释放,获取同步成功,在开始执行任务前,由于存在多个资源。因而更加积极,他通知后置节点线程从阻塞中唤醒,如果后置节点同样获取同步状态成功,则相当于同时有多个人在执行,也可以说释放锁可以多个线程进入。且这种方式具有传播性。直到某个节点获取同步状态失败才停止这种传播。

相同点

无论是独占和共享都提供了模板方法去定制能否获取同步和能否释放同步。

小结

能否获取同步决定了同时又多少人同时执行。和资源相关和独占式还是共享式无关。对于当个资源我们通常使用独占式,对于多个资源我们通常使用共享式。又时候你甚至将获取同步写成默认返回true,表示无锁。就好像是一个闸门绝对能放如多少水进入水库。是不是独占也能实现共享锁呢?答案是可以的。只不过他并没有共享式那么积极!

上一篇 下一篇

猜你喜欢

热点阅读