スレッドセーフLIFOデータ構造の実装
1. 前書き
このチュートリアルでは、we’ll discuss various options for Thread-safe LIFO Data structure implementations。
LIFOデータ構造では、要素は後入れ先出しの原則に従って挿入および取得されます。 これは、最後に挿入された要素が最初に取得されることを意味します。
コンピュータサイエンスでは、stackはそのようなデータ構造を指すために使用される用語です。
stackは、式の評価、元に戻す操作の実装など、いくつかの興味深い問題を処理するのに便利です。 同時実行環境で使用できるため、スレッドセーフにする必要があるかもしれません。
2. Stacksを理解する
基本的に、Stack must implement the following methods:
-
push() –上部に要素を追加します
-
pop() –最上位の要素をフェッチして削除します
-
peek() –基になるコンテナから削除せずに要素をフェッチします
前に説明したように、コマンド処理エンジンが必要だとしましょう。
このシステムでは、実行されたコマンドを元に戻すことが重要な機能です。
一般に、すべてのコマンドはスタックにプッシュされ、その後、元に戻す操作を簡単に実装できます。
-
最後に実行されたコマンドを取得するためのpop()メソッド
-
ポップされたコマンドオブジェクトでundo()メソッドを呼び出します
3. Stacksでのスレッドセーフの理解
If a data structure is not thread-safe, when accessed concurrently, it might end up having race conditions。
一言で言えば、競合状態は、コードの正しい実行がスレッドのタイミングとシーケンスに依存する場合に発生します。 これは主に、複数のスレッドがデータ構造を共有し、この構造がこの目的のために設計されていない場合に発生します。
以下のJavaコレクションクラスArrayDequeのメソッドを調べてみましょう。
public E pollFirst() {
int h = head;
E result = (E) elements[h];
// ... other book-keeping operations removed, for simplicity
head = (h + 1) & (elements.length - 1);
return result;
}
上記のコードの潜在的な競合状態を説明するために、次のシーケンスで示すように、このコードを実行する2つのスレッドを想定します。
-
最初のスレッドは3行目を実行します。resultオブジェクトに、インデックス「head」の要素を設定します
-
2番目のスレッドは3行目を実行します。resultオブジェクトに、インデックス´headの要素を設定します
-
最初のスレッドは5行目を実行します。インデックス「head」をバッキング配列の次の要素にリセットします
-
2番目のスレッドは5行目を実行します。インデックス「head」をバッキング配列の次の要素にリセットします
おっとっと! これで、両方の実行で同じ結果オブジェクト. が返されます。
このような競合状態を回避するために、この場合、他のスレッドが5行目の「head」インデックスのリセットを完了するまで、スレッドは最初の行を実行しないでください。 言い換えると、インデックス「head」の要素にアクセスし、インデックス「head」をリセットすることは、スレッドに対してアトミックに行われる必要があります。
明らかに、この場合、コードの正しい実行はスレッドのタイミングに依存するため、スレッドセーフではありません。
4. ロックを使用したスレッドセーフスタック
このセクションでは、スレッドセーフなstack. を具体的に実装するための2つの可能なオプションについて説明します。
特に、JavaのStack とスレッドセーフな装飾が施されたArrayDeque. について説明します。
どちらも、相互に排他的なアクセスにLocksを使用します.
4.1. JavaStackの使用
Javaコレクションには、基本的にArrayList.の同期バリアントであるVectorに基づいた、スレッドセーフなStackのレガシー実装があります。
ただし、公式ドキュメント自体は、ArrayDequeの使用を検討することを提案しています。 したがって、あまり詳しくは説明しません。
JavaのStackはスレッドセーフで簡単に使用できますが、このクラスには大きな欠点があります。
-
初期容量の設定はサポートされていません
-
すべての操作にロックを使用します。 これにより、シングルスレッド実行のパフォーマンスが低下する可能性があります。
4.2. ArrayDequeの使用
Using the Deque interface is the most convenient approach for LIFO data structures as it provides all the needed stack operations.ArrayDequeは、そのような具体的な実装の1つです.
操作にロックを使用していないため、シングルスレッドの実行は問題なく機能します。 しかし、マルチスレッド実行の場合、これには問題があります。
ただし、ArrayDeque.の同期デコレータを実装できます。これはJava Collection FrameworkのStackクラスと同様に機能しますが、Stackクラスの重要な問題である初期容量設定の欠如は解決されています。
このクラスを見てみましょう。
public class DequeBasedSynchronizedStack {
// Internal Deque which gets decorated for synchronization.
private ArrayDeque dequeStore;
public DequeBasedSynchronizedStack(int initialCapacity) {
this.dequeStore = new ArrayDeque<>(initialCapacity);
}
public DequeBasedSynchronizedStack() {
dequeStore = new ArrayDeque<>();
}
public synchronized T pop() {
return this.dequeStore.pop();
}
public synchronized void push(T element) {
this.dequeStore.push(element);
}
public synchronized T peek() {
return this.dequeStore.peek();
}
public synchronized int size() {
return this.dequeStore.size();
}
}
このソリューションには、さらに多くのメソッドが含まれているため、簡単にするためにDeque自体を実装していないことに注意してください。
また、GuavaにはSynchronizedDeque が含まれています。これは、装飾されたArrayDequeue.の本番環境に対応した実装です。
5. ロックフリースレッドセーフスタック
ConcurrentLinkedDequeは、Dequeインターフェースのロックフリー実装です。 efficient lock-free algorithm.を使用するため、This implementation is completely thread-safe
ロックフリーの実装は、ロックベースの実装とは異なり、次の問題の影響を受けません。
-
Priority inversion –これは、優先度の低いスレッドが優先度の高いスレッドに必要なロックを保持している場合に発生します。 これにより、優先度の高いスレッドがブロックされる可能性があります
-
Deadlocks –これは、異なるスレッドが同じリソースのセットを異なる順序でロックする場合に発生します。
さらに、ロックフリー実装には、シングルスレッド環境とマルチスレッド環境の両方で使用するのに最適な機能がいくつかあります。
-
非共有データ構造およびsingle-threaded access, performance would be at par with ArrayDequeの場合
-
共有データ構造の場合、パフォーマンスvaries according to the number of threads that access it simultaneously。
また、使いやすさの点では、どちらもDequeインターフェイスを実装しているため、ArrayDequeと同じです。
6. 結論
この記事では、stack data構造と、コマンド処理エンジンや式エバリュエーターなどのシステムの設計におけるその利点について説明しました。
また、Javaコレクションフレームワークのさまざまなスタック実装を分析し、それらのパフォーマンスとスレッドセーフのニュアンスについて説明しました。
いつものように、コード例はover on GitHubにあります。