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

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同样功能。

  • 当获取同步状态的线程调用condition.await(),当前线程会阻塞,并进入一个等待队列,释放同步状态.

  • 当其他线程调用了condition.signal()方法,会从等待队列firstWaiter开始选择第一个等待状态不是取消的节点.添加到同步队列尾部.

  • 当其他线程调用了condition.signalAll()方法,会从等待队列firstWaiter开始选择所有等待状态不是取消的节点.添加到同步队列尾部.

  • 这里取消节点表示当前节点的线程不在参与排队获取锁。

条件队列.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中方式锁的实现。

  • 无论独占式还时共享式获取同步状态成功则直接返回,

  • 当线程获取同步状态失败时,AQS则会将当前线程构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,

  • 当同步状态被释放时,会把首节点后第一个节点的线程从阻塞状态下唤醒,唤醒的线程会尝试竞争同步状态,如果获取同步状态成功,则从同步队列中出队,如果获取同步状态失败则继续阻塞。

同步队列.png
  • 当获取同步状态的线程调用condition.await(),当前线程会阻塞,并进入一个等待队列,释放同步状态.。

  • 当其他线程调用了condition.signal()方法,会从等待队列firstWaiter开始选择第一个等待状态不是取消的节点.添加到同步队列尾部.

  • 当其他线程调用了condition.signalAll()方法,会从等待队列firstWaiter开始选择所有等待状态不是取消的节点.添加到同步队列尾部.

  • 这里取消节点表示当前节点的线程不在参与排队获取锁。

条件队列.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则获得同步状态成功,否则获取失败。

调用场景:

  • 1 在acquire开始判断获取同步状态时调用

  • 2 在acquire自旋循环中每次都会获取同步状态时调用(该线程从阻塞中释放,会在自旋中重新竞争同步状态)

tryRelease

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

调用场景:

  • 1 该方法在释放独占式同步状态【release方法】时

共享式

tryAcquireShared

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

调用场景:

  • 1 在acquireShared开始判断获取同步状态时调用。

  • 2 在acquireShared自旋循环中每次都会获取同步状态时调用

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

tryReleaseShared

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

调用场景:

  • 该方法在释放共享式同步状态【releaseShared方法】时会调用。

独占式 VS 共享式

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

流程区别

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

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

独占式

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

共享式

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

相同点

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

小结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,578评论 6 544
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,701评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,691评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,974评论 1 318
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,694评论 6 413
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,026评论 1 329
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,015评论 3 450
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,193评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,719评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,442评论 3 360
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,668评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,151评论 5 365
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,846评论 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,255评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,592评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,394评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,635评论 2 380

推荐阅读更多精彩内容