1.AQSの概要
AbstractQueuedSynchronizer抽象クラス(以下、AQSと呼びます)はjava.util.concurrent
、パッケージ全体の中核です。JDK 1.5では、Doug LeaがJUCパッケージを導入し、パッケージ内のほとんどのシンクロナイザーはAQSに基づいて構築されました。AQSフレームワークは、同期状態の管理、スレッドのブロック/ウェイクアップ、および待機キューの管理を行うための一連の一般的なメカニズムを提供します。
ReentrantLock、CountDownLatch、CyclicBarrierなどのよく知られたシンクロナイザーは、実際には、内部クラスを介してAQSフレームワークによって公開されるAPIを実装し、さまざまなシンクロナイザー機能を実現します。これらのシンクロナイザーの主な違いは、実際には同期状態の定義です。
AQSフレームワークは、シンクロナイザーを構築するときに一連の懸念事項を分離します。そのすべての操作は、リソース(同期状態)を中心に展開し、ユーザーの次の問題を解決します。
- リソースに同時にアクセスできますか?または、同時に1つのスレッドからのみアクセスできますか?(共有/専用機能)
- リソースにアクセスするスレッドを同時に管理するにはどうすればよいですか?(待ち行列)
- スレッドがリソースを待機できない場合、待機キューを終了するにはどうすればよいですか?(タイムアウト/割り込み)
これは実際には典型的なテンプレートメソッドデザインパターンです。親クラス(AQSフレームワーク)がスケルトンと内部操作の詳細を定義し、特定のルールがサブクラスによって実装されます。
AQSフレームワークは、ユーザーに1つの質問を残します。それ
は、リソースとは何ですか。リソースにアクセスできるかどうかを定義するにはどうすればよいですか?
いくつかの一般的なシンクロナイザーのこの問題の定義を見てみましょう。
シンクロナイザー | リソースの定義 |
---|---|
ReentrantLock | リソースは排他ロックを表します。状態0はロックが使用可能であることを意味し、1は占有されていることを意味し、Nは再参加者の数を意味します |
CountDownLatch | リソースはカウントダウンカウンターを表します。状態0は、カウンターがゼロにリセットされ、すべてのスレッドがリソースにアクセスできることを意味します。状態Nは、カウンターがゼロにリセットされず、すべてのスレッドをブロックする必要があることを意味します。 |
セマフォ | リソースはセマフォまたはトークンを表します。状態≤0は、使用可能なトークンがなく、すべてのスレッドをブロックする必要があることを意味します。0より大きい場合は、トークンが使用可能であることを意味します。スレッドがトークンを取得するたびに、状態は1ずつ減少し、スレッドはトークンを解放しません。 、および状態が1増加します。 |
ReentrantReadWriteLock | リソースは、共有読み取りロックと排他的書き込みロックを表します。状態は論理的に2つの16ビット符号なしショートに分割され、読み取りロックによって使用されたスレッドの数と書き込みロックが再入力された回数を記録します。 |
要約すると、AQSフレームワークは次の機能を提供します。
1.1一連のテンプレートフレームワークを提供する
並行性が存在するため、考慮する必要のある状況が多数あります。したがって、ユーザー(AQSフレームワークのユーザー)にとって、多くの場合気にしないため、これら2つの目標を比較的簡単な方法で達成することが非常に重要です。内部の複雑な詳細。AQSは実際にテンプレートメソッドパターンを使用してこれを実現します。AQSのほとんどのメソッドは最終またはプライベートです。つまり、Doug Leaはユーザーがこれらのメソッドを直接使用することを望まず、テンプレートで指定されたメソッドの一部のみを上書きします。
AQSを使用すると、ユーザーは次のAPIを公開することで、上記の「リソースにアクセスできるかどうかを定義する方法」の問題を解決できます。
フック方式 | 説明 |
---|---|
tryAcquire | 排他的アクセス(リソースの数) |
tryRelease | 独占リリース(リソース数) |
tryAcquireShared | 共有取得(リソース数) |
tryReleaseShared | 共有取得(リソース数) |
isHeldExclusively | 独占ステータス |
1.2サポートの中断、タイムアウト
Lockインターフェイスでのロックの中断、時間制限のある待機、およびロックの試行の方法を覚えていますか?これらのメソッドの実現は、実際には組み込みのAQSによって提供されます。
AQSフレームワークを使用するすべてのシンクロナイザーは、次の操作をサポートします。
- ブロッキングおよび非ブロッキング(tryLockなど)の同期。
- オプションのタイムアウト設定。これにより、呼び出し元は待機をあきらめることができます。
- 割り込み可能なブロッキング操作。
1.3排他モードと共有モードのサポート
1.4サポート条件待機
Conditionインターフェースは、Obechctクラスのwait()、notify()、notifyAll()メソッドの代わりと見なすことができ、Lockと組み合わせて使用されます。
AQSフレームワークConditionObject
は、内部クラスを介してConditionインターフェースを実装し、サブクラスを条件付きで待機する機能を提供します。
2、AQSメソッドの説明
この章の最初の部分で述べたように、AQSはテンプレートメソッドパターンを使用し、そのほとんどは最終またはプライベートです。このタイプのメソッドを*スケルトンメソッド*と呼びます。これは、これらのメソッドがAQSフレームワーク自体によって定義されることを意味します。スケルトン、サブカテゴリは上書きできません。
以下では、いくつかのより重要な方法をカテゴリ別に簡単に説明します。具体的な実装の詳細と原則については、このシリーズの後続のパートで詳しく説明します。
2.1CASの運用
CAS、つまり、JavaでのCAS操作の実装であるCompareAndSetは、UnSafeという名前のクラスに委託されています。Unsafe
このクラスについては、今後詳細に導入される予定です。現在、ご存知のとおり、フィールドに対するアトミック操作です。このクラスを通じて達成することができます。
メソッド名 | 修飾子 | 説明 |
---|---|---|
compareAndSetState | 保護された決勝 | CASは同期状態の値を変更します |
compareAndSetHead | プライベートファイナル | CASは待機キューのヘッドポインタを変更します |
compareAndSetTail | プライベートファイナル | CASは待機キューのテールポインタを変更します |
compareAndSetWaitStatus | プライベートスタティックファイナル | CASはノードの待機状態を変更します |
compareAndSetNext | プライベートスタティックファイナル | CASはノードの次のポインタを変更します |
2.2待機キューのコア操作
メソッド名 | 修飾子 | 説明 |
---|---|---|
enq | 民間 | エンキュー操作 |
addWaiter | 民間 | エンキュー操作 |
setHead | 民間 | ヘッドノードを設定する |
unparkSuccessor | 民間 | 後続ノードをウェイクアップします |
doReleaseShared | 民間 | 共有ノードを解放する |
setHeadAndPropagate | 民間 | ヘッドノードを設定し、ウェイクアップを伝播します |
2.3リソースの取得
メソッド名 | 修飾子 | 説明 |
---|---|---|
cancelAcquire | 民間 | リソースへのアクセスをキャンセルする |
shouldParkAfterFailedAcquire | プライベートスタティック | 現在の呼び出しスレッドをブロックするかどうかを決定します |
取得キュー | 最後の | リソースを取得して失敗し、スレッドをブロックしてみてください |
doAcquireInterruptibly | 民間 | リソースへの排他的アクセス(中断への応答) |
doAcquireNanos | 民間 | リソースを排他的に取得します(限られた時間待ちます) |
doAcquireShared | 民間 | リソースへの共有アクセス |
doAcquireSharedInterruptibly | 民間 | リソースへの共有アクセス(中断への応答) |
doAcquireSharedNanos | 民間 | 共有リソース(期間限定) |
メソッド名 | 修飾子 | 説明 |
---|---|---|
取得する | パブリックファイナル | リソースへの排他的アクセス |
中断可能に取得 | パブリックファイナル | リソースへの排他的アクセス(中断への応答) |
中断可能に取得 | パブリックファイナル | リソースを排他的に取得します(限られた時間待ちます) |
取得共有 | パブリックファイナル | リソースへの共有アクセス |
AcquisitionSharedInterruptibly | パブリックファイナル | リソースへの共有アクセス(中断への応答) |
tryAcquireSharedNanos | パブリックファイナル | 共有リソース(期間限定) |
2.4リソースのリリース操作
メソッド名 | 修飾子 | 説明 |
---|---|---|
リリース | パブリックファイナル | 独占的なリソースを解放する |
releaseShared | パブリックファイナル | 共有リソースを解放する |
3.AQSの原則の簡単な説明
最初のセクションで述べたように、AQSフレームワークは、シンクロナイザーを構築するときに一連の懸念事項を分離します。そのすべての操作は、リソース(同期状態)を中心に展開します。したがって、リソースを中心に、3つの基本的な質問が導き出されます。
- 同期状態の管理
- スレッドのブロック/ウェイクアップ
- スレッド待機キュー管理
3.1同期ステータス
同期状態の定義同期状態は実際にはリソースです。AQSは、単一のint(32ビット)を使用して同期状態を保存し、getState、setState、およびcompareAndSetState操作を公開して、この状態を読み取って更新します。
/**
* 同步状态.
*/
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
/**
* 以原子的方式更新同步状态.
* 利用Unsafe类实现
*/
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
3.2スレッドのブロック/ウェイクアップ
JDK 1.5より前は、組み込みのモニターメカニズムを除いて、現在のスレッドを安全かつ便利にブロックしてウェイクアップする方法は他にありませんでした。
JDK1.5以降、java.util.concurrent.locksパッケージは、スレッドのブロックとウェイクアップのためのツールとしてLockSupportクラスを提供します。
3.3 等待队列
等待队列,是AQS框架的核心,整个框架的关键其实就是如何在并发状态下管理被阻塞的线程。
等待队列是严格的FIFO队列,是Craig,Landin和Hagersten锁(CLH锁)的一种变种,采用双向链表实现,因此也叫CLH队列。
1. 结点定义
CLH队列中的结点是对线程的包装,结点一共有两种类型:独占(EXCLUSIVE)和共享(SHARED)。
每种类型的结点都有一些状态,其中独占结点使用其中的CANCELLED(1)、SIGNAL(-1)、CONDITION(-2),共享结点使用其中的CANCELLED(1)、SIGNAL(-1)、PROPAGATE(-3)。
结点状态 | 值 | 描述 |
---|---|---|
CANCELLED | 1 | 取消。表示后驱结点被中断或超时,需要移出队列 |
SIGNAL | -1 | 发信号。表示后驱结点被阻塞了(当前结点在入队后、阻塞前,应确保将其prev结点类型改为SIGNAL,以便prev结点取消或释放时将当前结点唤醒。) |
CONDITION | -2 | Condition专用。表示当前结点在Condition队列中,因为等待某个条件而被阻塞了 |
PROPAGATE | -3 | 传播。适用于共享模式(比如连续的读操作结点可以依次进入临界区,设为PROPAGATE有助于实现这种迭代操作。) |
INITIAL | 0 | 默认。新结点会处于这种状态 |
AQS使用CLH队列实现线程的结构管理,而CLH结构正是用前一结点某一属性表示当前结点的状态,之所以这种做是因为在双向链表的结构下,这样更容易实现取消和超时功能。
next指针:用于维护队列顺序,当临界区的资源被释放时,头结点通过next指针找到队首结点。
prev指针:用于在结点(线程)被取消时,让当前结点的前驱直接指向当前结点的后驱完成出队动作。
static final class Node {
// 共享模式结点
static final Node SHARED = new Node();
// 独占模式结点
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
/**
* INITAL: 0 - 默认,新结点会处于这种状态。
* CANCELLED: 1 - 取消,表示后续结点被中断或超时,需要移出队列;
* SIGNAL: -1- 发信号,表示后续结点被阻塞了;(当前结点在入队后、阻塞前,应确保将其prev结点类型改为SIGNAL,以便prev结点取消或释放时将当前结点唤醒。)
* CONDITION: -2- Condition专用,表示当前结点在Condition队列中,因为等待某个条件而被阻塞了;
* PROPAGATE: -3- 传播,适用于共享模式。(比如连续的读操作结点可以依次进入临界区,设为PROPAGATE有助于实现这种迭代操作。)
*
* waitStatus表示的是后续结点状态,这是因为AQS中使用CLH队列实现线程的结构管理,而CLH结构正是用前一结点某一属性表示当前结点的状态,这样更容易实现取消和超时功能。
*/
volatile int waitStatus;
// 前驱指针
volatile Node prev;
// 后驱指针
volatile Node next;
// 结点所包装的线程
volatile Thread thread;
// Condition队列使用,存储condition队列中的后继节点
Node nextWaiter;
Node() {
}
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
}
2. 队列定义
对于CLH队列,当线程请求资源时,如果请求不到,会将线程包装成结点,将其挂载在队列尾部。
CLH队列的示意图如下:
①初始状态,队列head和tail都指向空
②首个线程入队,先创建一个空的头结点,然后以自旋的方式不断尝试插入一个包含当前线程的新结点
/**
* 以自旋的方式不断尝试插入结点至队列尾部
*
* @return 当前结点的前驱结点
*/
private Node enq(final Node node) {
for (; ; ) {
Node t = tail;
if (t == null) { // 如果队列为空,则创建一个空的head结点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
四、总结
本章简要介绍了AQS的思想和原理,读者可以参考Doug Lea的论文,进一步了解AQS。
本文参考:https://segmentfault.com/a/1190000015562787
关注公众号,输入“java-summary”即可获得源码。
完成,收工!
【传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。