Java マルチスレッド ポイントの概要 (Java の同時実行コンテナーとフレームワーク、アトミック操作クラス、同時実行ツール クラス)

ConcurrentHashMap の実装原理と使い方

ConcurrentHashMap は、スレッドセーフで効率的な HashMap です。並行プログラミングで HashMap を使用すると、プログラムの無限ループが発生する可能性があります。スレッドセーフな HashTable の使用効率は非常に低く、上記の 2 つの理由から、ConcurrentHashMap が登場する機会があります。
マルチスレッド環境では、put 操作に HashMap を使用すると無限ループが発生し、CPU 使用率が 100% に近くなるため、HashMap を並行状況で使用することはできません。たとえば、次のコードを実行すると、無限ループが発生します。

final HashMap<String, String> map = new HashMap<String, String>(2);
Thread t = new Thread(new Runnable() {
    
    
	@Override
	public void run() {
    
    
		for (int i = 0; i < 10000; i++) {
    
    
			new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				map.put(UUID.randomUUID().toString(), "");
				}
			}, "ftf" + i).start();
		}
	}
}, "ftf");
t.start();
t.join();

put 操作が同時に実行されると、HashMap によって無限ループが発生します。これは、マルチスレッドによって HashMap エントリ リンク リストがリング データ構造を形成するためです。リング データ構造が形成されると、エントリの次のノードが空になることはありません。となり、Entry を取得するために無限ループが発生します。

HashTable コンテナーは、同期を使用してスレッドの安全性を確保しますが、スレッドの競合が激しい場合、HashTable の効率は非常に低くなります。スレッドが HashTable の同期メソッドにアクセスし、他のスレッドも HashTable の同期メソッドにアクセスすると、ブロッキングまたはポーリング状態になるためです。たとえば、スレッド 1 は put を使用して要素を追加しますが、スレッド 2 は put メソッドを使用して要素を追加できないだけでなく、get メソッドを使用して要素を取得することもできないため、競合が激しくなればなるほど効率が低下します。
ConcurrentHashMap で使用されるセグメンテーション技術をロックしますまず、データをセクションに分割して1つずつ格納し、各セクションのデータにロックを割り当てます. スレッドがロックを占有して1つのセクションのデータにアクセスすると、他のスレッドから別のセクションのデータにもアクセスできます. .

ConcurrentHashMap は、Segment 配列構造と HashEntry 配列構造から構成されます。セグメントは再入可能ロック (ReentrantLock) であり、ConcurrentHashMap でロックの役割を果たす; HashEntry はキーと値のペア データを格納するために使用されます。ConcurrentHashMap には、Segment 配列が含まれています。セグメントの構造は、配列と連結リスト構造である HashMap に似ています。セグメントには HashEntry 配列が含まれます。各 HashEntry は、リンクされたリスト構造の要素です。各セグメントは、HashEntry 配列内の要素を保護します。HashEntry 配列のデータを変更するときは、最初に対応するセグメント ロックを取得する必要があります。
ここに画像の説明を挿入
ConcurrentHashMap 初期化メソッドは、initialCapacity、loadFactor、concurrencyLevel などのいくつかのパラメーターを使用して、各セグメントのセグメント配列、セグメント オフセット segmentShift、セグメント マスク、segmentMask、および HashEntry 配列を初期化することによって実装されます。
ConcurrentHashMap の操作
1. get 操作
セグメントの get 操作は非常に単純で効率的です。1 回のパスとハッシュの後、このハッシュ値を使用してハッシュ操作によってセグメントを見つけ、次にハッシュ アルゴリズムによって要素を見つけます。コードは次のようになります。

public V get(Object key) {
    
    
	int hash = hash(key.hashCode());
	return segmentFor(hash).get(key, hash);
}

取得操作の効率は、取得プロセス全体をロックする必要がないことです。読み取り値が空でない限り、ロックされて再読み取りされます。get メソッドで使用される共有変数はすべて volatile 型として定義されています. volatile として定義された変数は、スレッド間の可視性を維持でき、複数のスレッドで同時に読み取ることができ、期限切れの値を読み取らないことが保証されていますが、単一のスレッド書き込みで使用されます (1 つのケースを複数のスレッドで書き込むことができます。つまり、書き込まれた値は元の値に依存しません)。get 操作では、共有変数のカウントと値を読み取るだけで、書き込む必要はありません。ので、ロックする必要はありません。期限切れの値が読み取られない理由は、Java メモリ モデルの発生前の原則に従って、2 つのスレッドが一時的に volatile 変数を変更および取得したとしても、volatile フィールドへの書き込み操作が読み取り操作の前に行われるためです。同時に、取得操作は最新の値も取得できます。これは、ロックを揮発性に置き換える古典的なアプリケーション シナリオです。
2. put 操作 put
メソッドはシェア変数に書き込む必要があるため、スレッドセーフのため、シェア変数の操作時にロックを追加する必要があります
put メソッドは、最初にセグメントを見つけてから、セグメントで挿入操作を実行します。挿入操作は 2 つのステップを経る必要があります
. 最初のステップは、セグメント内の HashEntry 配列を拡張する必要があるかどうかを判断することです. 2 番目のステップは、追加された要素の位置を特定し、それを HashEntry 配列に配置することです
.
3. サイズ操作
ConcurrentHashMap 全体の要素のサイズをカウントする場合は、すべてのセグメントの要素のサイズをカウントして合計する必要があります。カウントを累積する過程で、以前に累積されたカウントが変化する確率は非常に小さいため、ConcurrentHashMap のメソッドは、セグメントをロックせずに各セグメントのサイズをカウントするために 2 回試行することです。カウントのプロセス 変更がある場合は、ロックを使用してすべてのセグメントのサイズをカウントします。
ConcurrentLinkedQueue
ConcurrentLinkedQueue は、リンクされたノードに基づく無制限のスレッドセーフなキューです. ノードをソートするために先入れ先出しルールを使用します. 要素を追加すると、キューの末尾に追加されます.キューの先頭にある要素を返します。これは、Michael&Scott アルゴリズムにいくつかの変更を加えた「ウェイトフリー」アルゴリズム (つまり、CAS アルゴリズム) を使用して実装します。

ConcurrentLinkedQueue は、先頭ノードと末尾ノードで構成されます. 各ノード (Node) は、ノード要素 (item) と次のノードへの参照 (next) で構成されます. この next を介してノードが接続され、リンクされたリストが形成されます.構造のキュー。デフォルトでは、ヘッド ノードに格納されている要素は空で、テール ノードはヘッド ノードと同じです。

エンキュー:
エンキューとは、キューの最後にエンキュー ノードを追加することです。チームに参加すると、主に 2 つのことが行われます: 1 つ目は、現在のキューのテール ノードの次のノードとして参加ノードを設定することです。2 つ目は、テール ノードを更新することです。テール ノードの次のノードが空でない場合は、ノードをテール ノードに結合し、テール ノードの次のノードが空の場合、エンキュー ノードをテールの次のノードに設定します。したがって、テール ノードが常にテール ノードであるとは限りません。

キューから:
キューからノード要素を返し、要素へのノードの参照をクリアします。キューが解放されるたびにヘッドノードを更新する必要はありません.ヘッドノードに要素がある場合、ヘッドノードを更新せずにヘッドノードの要素が直接ポップアップされます. ヘッド ノードに要素がない場合のみ、デキュー操作は
ヘッド ノード Java のブロッキング キューを更新します。
ブロッキング キュー (BlockingQueue) は、2 つの追加操作をサポートするキューです。これら 2 つの追加操作は、挿入と削除の方法をブロックすることをサポートします。
1) ブロッキングをサポートする挿入方法: キューがいっぱいになると、キューがいっぱいになるまで要素を挿入するスレッドをキューがブロックすることを意味します。
2) ブロッキングをサポートする削除方法: キューが空の場合、要素を取得するスレッドはキューが空でなくなるまで待機することを意味します。
ブロッキング キューは、プロデューサーとコンシューマーのシナリオでよく使用されます. プロデューサーはキューに要素を追加するスレッドであり、コンシューマーはキューから要素を取得するスレッドです. ブロッキング キューは、プロデューサーが要素を格納するために使用し、コンシューマーが要素を取得するために使用するコンテナーです。
ここに画像の説明を挿入
JDK 7 には、次の 7 つのブロッキング キューが用意されています。
ArrayBlockingQueue: 配列構造で構成される制限付きブロッキング キュー。
LinkedBlockingQueue: リンクされたリスト構造で構成される制限付きブロッキング キュー。
PriorityBlockingQueue: 優先度の並べ替えをサポートする無制限のブロッキング キュー。
DelayQueue: プライオリティ キューを使用して実装された無制限のブロッキング キュー。
SynchronousQueue: 要素を格納しないブロッキング キュー。
LinkedTransferQueue: リンクされたリスト構造で構成される無制限のブロッキング キュー。
LinkedBlockingDeque: リンクされたリスト構造で構成される双方向ブロッキング キュー。

Fork/Join フレームワーク
Fork/Join フレームワークは、Java 7 が提供するタスクを並列実行するためのフレームワークで、大きなタスクをいくつかの小さなタスクに分割し、最終的にそれぞれの小さなタスクの結果を要約して結果を得るフレームワークです。大きなタスク。
ここに画像の説明を挿入
ワークスティーリング アルゴリズムとは、実行のために他のキューからタスクを盗むスレッドを指します。比較的大きなタスクを実行する必要がある場合, このタスクをいくつかの独立したサブタスクに分割できます. スレッド間の競合を減らすために, これらのサブタスクを別のキューに入れ, 各キューにキューを作成します. 別のスレッドがタスクを実行しますスレッドとキューの間には 1 対 1 の対応があります。たとえば、スレッド A は、キュー A 内のタスクの処理を担当しています。ただし、一部のスレッドは自分のキュー内のタスクを最初に終了しますが、他のスレッドに対応するキュー内で処理されるのを待っているタスクがまだあります。待機する代わりに、作業を完了したスレッドは他のスレッドの作業を支援する可能性があるため、他のスレッドのキューに移動して
実行するタスクを盗みます。このとき、同じキューにアクセスするため、盗むタスクスレッドと盗まれるタスクスレッドの競合を減らすために、通常は両端キューが使用され、盗まれたタスクスレッドは常に先頭からタスクを実行します。タスクをスチールするスレッドは、常に両端キューの末尾からタスクを実行します。
ワークスティーリング操作プロセス:
ここに画像の説明を挿入
ワークスティーリング アルゴリズムの利点: 並列計算にスレッドを最大限に活用し、スレッド間の競合を減らします。
ワークスティーリング アルゴリズムの欠点: 両端キューにタスクが 1 つしかない場合など、場合によってはまだ競合が発生します。また、アルゴリズムは、複数のスレッドや複数の両端キューを作成するなど、より多くのシステム リソースを消費します。
Fork/Join フレームワークの設計:
ステップ 1 タスクを分割します。まず、大きなタスクをサブタスクに分割する fork クラスが必要です.サブタスクがまだ非常に大きい可能性があるため、サブタスクが十分に小さくなるまで分割を続ける必要があります.
ステップ 2 タスクを実行し、結果を結合します。分割されたサブタスクは両端キューに配置され、複数の起動スレッドが両端キューからタスクを取得して実行します。サブタスクの結果はすべてキューに入れられ、スレッドが開始されてキューからデータが取得され、データがマージされます。
Fork/Join は、上記の 2 つのことを達成するために 2 つのクラスを使用します。
①ForkJoinTask: ForkJoin フレームワークを使用するには、まず ForkJoin タスクを作成する必要があります。タスクで fork() および join() 操作を実行するメカニズムを提供します。通常、ForkJoinTask クラスを直接継承する必要はなく、そのサブクラスを継承するだけで済みます.Fork/Join フレームワークは、次の 2 つのサブクラスを提供します。
· RecursiveAction: 結果を返さないタスク用。
· RecursiveTask: 結果を返すタスク用。
②ForkJoinPool: ForkJoinTask は ForkJoinPool を通じて実行する必要があります。
タスクから分割されたサブタスクは、現在のワーカー スレッドによって維持される両端キューに追加され、キューの先頭に入ります。ワーカー スレッドのキューにタスクがない場合、他のワーカー スレッドのキューの末尾からランダムにタスクを取得します。

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
public class CountTask extends RecursiveTask<Integer> {
    
    
	private static final int THRESHOLD = 2; // 阈值
	private int start;
	private int end;
	public CountTask(int start, int end) {
    
    
		this.start = start;
		this.end = end;
	}
	@Override
	protected Integer compute() {
    
    
		int sum = 0;
		// 如果任务足够小就计算任务
		boolean canCompute = (end - start) <= THRESHOLD;
		if (canCompute) {
    
    
		for (int i = start; i <= end; i++) {
    
    
			sum += i;
			}
		} else {
    
    
		// 如果任务大于阈值,就分裂成两个子任务计算
	int middle = (start + end) / 2;
	CountTask leftTask = new CountTask(start, middle);
	CountTask rightTask = new CountTask(middle + 1, end);
	// 执行子任务
	leftTask.fork();
	rightTask.fork();
	// 等待子任务执行完,并得到其结果
	int leftResult=leftTask.join();
	int rightResult=rightTask.join();
	// 合并子任务
	sum = leftResult + rightResult;
	}
	return sum;
}
public static void main(String[] args) {
    
    
	ForkJoinPool forkJoinPool = new ForkJoinPool();
	// 生成一个计算任务,负责计算1+2+3+4
	CountTask task = new CountTask(1, 4);
	// 执行一个任务
	Future<Integer> result = forkJoinPool.submit(task);
	try {
    
    
		System.out.println(result.get());
	} catch (InterruptedException e) {
    
    
	} catch (ExecutionException e) {
    
    
	}
	}
}

ForkJoinTask と一般的なタスクの主な違いは、compute メソッドを実装する必要があることです. このメソッドでは、最初にタスクが十分に小さいかどうかを判断し、十分に小さい場合はタスクを直接実行する必要があります. 十分に小さくない場合は、2 つのサブタスクに分割する必要があります.各サブタスクが fork メソッドを呼び出すと、現在のサブタスクをサブタスクに分割する必要があるかどうかを確認するために、compute メソッドに入ります.続行する必要がない場合分割されたら、現在のサブタスクを実行し、結果を返します。join メソッドを使用すると、サブタスクが完了してその結果が得られるまで待機します。

Fork/Join フレームワークの実装原理:
ForkJoinPool は ForkJoinTask 配列と ForkJoinWorkerThread 配列で構成され、ForkJoinTask 配列は ForkJoinPool へのストレージ プログラムの送信を担当し、ForkJoinWorkerThread 配列はこれらのタスクの実行を担当します。

Java の 13 個のアトミック操作クラス

Java は JDK 1.5 から java.util.concurrent.atomic パッケージ (以下、Atomic パッケージと呼びます) を提供しており、このパッケージのアトミック操作クラスは、変数を更新する方法を簡単な使用法、高性能、およびスレッドセーフに提供します。Atomic パッケージは、合計 13 のクラスを提供します。これらのクラスは、4 種類のアトミック更新メソッド、つまり、アトミック更新基本型、アトミック更新配列、アトミック更新参照、およびアトミック更新属性 (フィールド) に属します。Atomic パッケージのクラスは、基本的に Unsafe を使用して実装されたラッパー クラスです。
アトミック更新基本型クラス:
AtomicBoolean: アトミック更新ブール型。
·AtomicInteger: アトミック更新整数。
·AtomicLong: アトミック更新長整数。
AtomicInteger を例に説明すると、AtomicInteger の一般的なメソッドは次のとおりです。
· int addAndGet (int delta): 入力値をインスタンスの値 (AtomicInteger の値) に原子的に追加し、結果を返します。
boolean compareAndSet(int expect, int update): 入力された値が期待値と等しい場合、値を入力された値にアトミックに設定します。
·int getAndIncrement(): アトミックな方法で現在の値を 1 ずつインクリメントします。ここで返される値は、自動インクリメントの前の値であることに注意してください。
· void lazySet (int newValue): 最終的には newValue に設定されます。値を設定するために lazySet を使用した後、他のスレッドはまだ短時間の間古い値を読み取ることができる場合があります · int getAndSet (int newValue): アトミックに設定され
ますnewValue の値で、古い値を返します。

getAndIncrement はアトミック操作をどのように実装しますか?

public final int getAndIncrement() {
    
    
	for (;;) {
    
    
		int current = get();
		int next = current + 1;
		if (compareAndSet(current, next))
			return current;
		}
}
public final boolean compareAndSet(int expect, int update) {
    
    
	return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

ソース コードの for ループ本体の最初のステップは、AtomicInteger に格納されている値を取得することです。2 番目のステップは、AtomicInteger の現在の値に 1 を追加することです。重要な 3 番目のステップは、compareAndSet メソッドを呼び出して実行します。アトミック更新操作. このメソッドは、最初に現在の値が現在と等しいかどうかをチェックし、等しい場合は、AtomicInteger の値が他のスレッドによって変更されていないことを意味し、次に AtomicInteger の現在の値を next の値に更新します。 compareAndSet メソッドが false を返すと、プログラムは for ループに入り、compareAndSet 操作を再度実行します。
アトミック更新配列:
アトミックな方法で配列内の要素を更新します. アトミック パッケージは以下を提供します:
·AtomicIntegerArray: 整数配列内の要素をアトミック更新します.
AtomicLongArray: 長整数配列の要素をアトミックに更新します。
AtomicReferenceArray: 参照型配列内の要素をアトミックに更新します

· AtomicIntegerArray クラスは、主に配列内の整数を更新するアトミックな方法を提供します。その一般的なメソッドは次のとおりです。
· int addAndGet(int i, int delta): 配列のインデックス i の要素に入力値を原子的に追加します。
· boolean compareAndSet(int i, int expect, int update): 現在の値が期待値と等しい場合、配列位置 i の要素をアトミックに更新値に設定します。

public class AtomicIntegerArrayTest {
    
    
	static int[] value = new int[] {
    
     12 };
	static AtomicIntegerArray ai = new AtomicIntegerArray(value);
	public static void main(String[] args) {
    
    
		ai.getAndSet(03);
		System.out.println(ai.get(0));
		System.out.println(value[0]);
	}
}

出力結果:

3
1

配列値は構築メソッドを介して渡され、AtomicIntegerArray は現在の配列をコピーするため、AtomicIntegerArray が内部配列要素を変更しても、受信配列には影響しません。
原子更新参照型
基本型 AtomicInteger の原子更新は 1 つの変数しか更新できません. 複数の変数を原子的に更新したい場合は, この原子更新参照型が提供するクラスを利用する必要があります. Atomic パッケージは、次の 3 つのクラスを提供します。
AtomicReference: アトミック更新参照タイプ。
· AtomicReferenceFieldUpdater: 参照型のフィールドをアトミ​​ックに更新します。
AtomicMarkableReference: アトミック更新用のマーク付きビットを持つ参照型。ブール値のタグ ビットと参照型をアトミックに更新することが可能です。構築方法は AtomicMarkableReference(V initialRef, boolean initialMark) です。
例として AtomicReference を取り上げます。

public class AtomicReferenceTest {
    
    
	public static AtomicReference<user> atomicUserRef = new AtomicReference<user>();
	public static void main(String[] args) {
    
    
		User user = new User("conan"15);
		atomicUserRef.set(user);
		User updateUser = new User("Shinichi"17);
		atomicUserRef.compareAndSet(user, updateUser);
		System.out.println(atomicUserRef.get().getName());
		System.out.println(atomicUserRef.get().getOld());
	}
	static class User {
    
    
		private String name;
		private int old;
		public User(String name, int old) {
    
    
			this.name = name;
			this.old = old;
		}
		public String getName() {
    
    
			return name;
		}
		public int getOld() {
    
    
			return old;
		}
	}
}

コードでは、最初にユーザー オブジェクトを構築し、次にユーザー オブジェクトを AtomicReferenc に設定し、最後に compareAndSet メソッドを呼び出してアトミック更新操作を実行します. 実装原理は AtomicInteger の compareAndSet メソッドと同じです. コードの実行後、出力結果は次のようになります。

Shinichi
17

アトミック更新フィールド クラス
特定のクラスのフィールドをアトミ​​ックに更新する必要がある場合は、アトミック更新フィールド クラスを使用する必要があります. Atomic パッケージは、アトミック フィールド更新用に次の 3 つのクラスを提供します.
AtomicIntegerFieldUpdater: 整数フィールドをアトミ​​ックに更新するアップデーター。
AtomicLongFieldUpdater: 長整数フィールドをアトミ​​ックに更新するアップデーター。
· AtomicStampedReference: バージョン番号を持つアトミック更新参照タイプ。このクラスは、アトミック更新データとデータ バージョン番号に使用できる参照に整数値を関連付け、アトミック更新に CAS を使用する場合に発生する可能性がある ABA 問題を解決できます。

フィールド クラスを原子的に更新するには、2 つの手順が必要です。最初のステップでは、アトミック更新フィールド クラスは抽象クラスであるため、使用するたびに静的メソッド newUpdater() を使用して更新プログラムを作成し、更新するクラスと属性を設定する必要があります。2 番目のステップでは、更新クラスのフィールド (プロパティ) で public volatile 修飾子を使用する必要があります。

public class AtomicIntegerFieldUpdaterTest {
    
    
	// 创建原子更新器,并设置需要更新的对象类和对象的属性
	private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.	newUpdater(User.class"old");
	public static void main(String[] args) {
    
    
		// 设置柯南的年龄是10岁
		User conan = new User("conan"10);
		// 柯南长了一岁,但是仍然会输出旧的年龄
		System.out.println(a.getAndIncrement(conan));
	// 输出柯南现在的年龄
	System.out.println(a.get(conan));
}
	public static class User {
    
    
		private String name;
		public volatile int old;
		public User(String name, int old) {
    
    
			this.name = name;
			this.old = old;
		}
		public String getName() {
    
    
			return name;
		}
		public int getOld() {
    
    
			return old;
		}
	}
}

出力結果:

10
11

Java の同時実行ツール

CountDownLatch、CyclicBarrier、および Semaphore ツール クラスは同時プロセス制御の手段を提供し、Exchanger ツール クラスはスレッド間でデータを交換する手段を提供します。
複数のスレッドが完了するまで待機する CountDownLatch
CountDownLatch を使用すると、1 つ以上のスレッドが他のスレッドが操作を完了するまで待機できます。そのような要件がある場合: Excel で複数のシートのデータを分析する必要がある場合、この時点でマルチスレッドの使用を検討できます。各スレッドはシート内のデータを解析し、すべてのシートが解析された後、プログラム解析が完了したことを確認する必要があります。この要件では、メイン スレッドがすべてのスレッドがシートの解析操作を完了するまで待機することを理解する最も簡単な方法は、コード リストに示すように、join() メソッドを使用することです。

public class JoinCountDownLatchTest {
    
    
	public static void main(String[] args) throws InterruptedException {
    
    
		Thread parser1 = new Thread(new Runnable() {
    
    
		@Override
		public void run() {
    
    
		}
	});
		Thread parser2 = new Thread(new Runnable() {
    
    
		@Override
		public void run() {
    
    
			System.out.println("parser2 finish");
		}
	});
		parser1.start();
		parser2.start();
		parser1.join();
		parser2.join();
		System.out.println("all parser finish");
	}
}

join は、現在の実行スレッドが、結合スレッドの実行が終了するまで待機できるようにするために使用されます。実装の原則は、結合スレッドが生きているかどうかをチェックし続け、結合スレッドが生きている場合は、現在のスレッドを永久に待機させることです。結合スレッドが終了するまで、スレッドの this.notifyAll() メソッドが呼び出されます。

CountDownLatch は join の機能を実装することもでき、join よりも多くの機能を備えています。

public class CountDownLatchTest {
    
    
	staticCountDownLatch c = new CountDownLatch(2);
	public static void main(String[] args) throws InterruptedException {
    
    
		new Thread(new Runnable() {
    
    
		@Override
		public void run() {
    
    
				System.out.println(1);
				c.countDown();
				System.out.println(2);
				c.countDown();
			}
		}).start();
	c.await();
	System.out.println("3");
	}
}

CountDownLatch のコンストラクターは int 型のパラメーターをカウンターとして受け取ります. N ポイントが完了するのを待ちたい場合は、ここで N を渡します.
CountDownLatch の countDown メソッドを呼び出すと、N が 1 減少し、CountDownLatch の await メソッドは、N がゼロになるまで現在のスレッドをブロックします。countDown メソッドはどこでも使用できるため、ここで述べた N ポイントは N スレッド、または 1 スレッドの N 実行ステップである可能性があります。複数のスレッドで使用する場合、CountDownLatch 参照をスレッドに渡すだけで済みます。
同期バリア CyclicBarrier
CyclicBarrier 文字通り、リサイクル可能な (Cyclic) バリア (Barrier) を意味します。スレッドがバリア (同期ポイントとも呼ばれます) に到達すると、スレッドのグループをブロックする必要があります. バリアは、最後のスレッドがバリアに到達するまで開かれず、バリアによってブロックされたすべてのスレッドは実行を続けます.
デフォルトの構築メソッドは CyclicBarrier (int パーティー) で、そのパラメーターは、バリアによってインターセプトされたスレッドの数を示します. 各スレッドは await メソッドを呼び出して、CyclicBarrier にバリアに到達したことを通知し、現在のスレッドがブロックされます.

public class CyclicBarrierTest {
    
    
	staticCyclicBarrier c = new CyclicBarrier(2);
	public static void main(String[] args) {
    
    
		new Thread(new Runnable() {
    
    
		@Override
		public void run() {
    
    
			try {
    
    
				c.await();
			} catch (Exception e) {
    
    
		}
		System.out.println(1);
	}
	}).start();
	try {
    
    
		c.await();
	} catch (Exception e) {
    
    
	}
	System.out.println(2);
	}
}

メインスレッドとサブスレッドのスケジューリングは CPU によって決定されるため、両方のスレッドが最初に実行される可能性があるため、2 つの出力、1 2つまり2 1. new CyclicBarrier(2) が new CyclicBarrier(3) に変更された場合、await メソッドを実行する 3 番目のスレッドがないため、つまり、3 番目のスレッドがバリアに到達しないため、メイン スレッドとサブスレッドは永久に待機します。 Threads will not continue to execute CyclicBarrier
. また、スレッドがバリアに到達したときに最初に barrierAction を実行するために使用される、より高度なコンストラクタ CyclicBarrier (int パーティー、Runnable barrierAction) も提供します。これは、より複雑な処理に便利です。コード リストに示すように、ビジネス シナリオ。

import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest2 {
    
    
	static CyclicBarrier c = new CyclicBarrier(2, new A());
	public static void main(String[] args) {
    
    
		new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				try {
    
    
					c.await();
				} catch (Exception e) {
    
    
			}
			System.out.println(1);
		}
	}).start();
		try {
    
    
			c.await();
		} catch (Exception e) {
    
    
	}
	System.out.println(2);
	}
	static class A implements Runnable {
    
    
		@Override
		public void run() {
    
    
		System.out.println(3);
		}
	}
}

CyclicBarrier は傍受するスレッドの数を 2 に設定しているため、コード内の最初のスレッドとスレッド A が実行されるまで待ってから、メイン スレッドの実行を続行し、2 を出力する必要があるため、コード実行後の出力は次のようになります。次のとおりです3 1 2CountDownLatch のカウンターは 1 回しか使用できませんが、CyclicBarrier のカウンターは reset() メソッドを使用してリセットできます。そのため、CyclicBarrier はより複雑なビジネス シナリオを処理できます。
同時スレッド数を制御する
セマフォ 特定のリソースに同時にアクセスするスレッドの数を制御するセマフォ (セマフォ) は、各スレッドを調整して、共通のリソースを適切に使用できるようにします。
たとえば、×× 道路の交通量は制限されています. この道路を同時に走行できるのは 100 台の車両のみであり、残りの車両は交差点で待機する必要があります. したがって、最初の 100 台の車両が青信号を見ることになります.この車は赤信号を見て XX 道路に入ることができませんが、最初の 100 台の車両のうち 5 台が XX 道路を離れた場合、5 台の車が後ろの道路に入ることができます。この例では スレッドです. 道路に入ると, スレッドが実行中であることを意味します. 道路を離れると, スレッドの実行が完了したことを意味します. 赤信号が見えたら, スレッドが実行中であることを意味します.ブロックされ、実行できません。

セマフォは、特にデータベース接続などの限られたパブリック リソースを使用するアプリケーション シナリオで、フロー制御に使用できます。数万のファイルのデータを読み取る必要がある場合、それらはすべて IO 集中型のタスクであるため、数十のスレッドを開始して同時に読み取ることができますが、それらがメモリに読み込まれる場合は、それらを次の場所に格納する必要があります。データベース、およびデータベース接続の数は 10 のみです。この時点で、データベース接続の取得とデータの保存を同時に行うために、10 のスレッドのみを制御する必要があります。そうしないと、エラーが報告され、データベース接続を取得できません。このとき、コード リストに示すように、フロー制御に Semaphore を使用できます。

public class SemaphoreTest {
    
    
	private static final int THREAD_COUNT = 30;
	private static ExecutorServicethreadPool = Executors.newFixedThreadPool(THREAD_COUNT);
	private static Semaphore s = new Semaphore(10);
	public static void main(String[] args) {
    
    
		for (inti = 0; i< THREAD_COUNT; i++) {
    
    
			threadPool.execute(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				try {
    
    
					s.acquire();
					System.out.println("save data");
					s.release();
			} catch (InterruptedException e) {
    
    
			}
			}
		});
	}
	threadPool.shutdown();
	}
}

30 個のスレッドが実行されていますが、同時に実行できるのは 10 個だけです。Semaphore のコンストラクタ Semaphore(int permits) は、利用可能なライセンス数を示す整数を受け入れます。Semaphore (10) は、10 個のスレッドがライセンスを取得できることを意味します。つまり、同時実行の最大数は 10 です。Semaphore の使い方も非常にシンプルで、スレッドはまず Semaphore の acquire() メソッドを使用してライセンスを取得し、使用後に release() メソッドを呼び出してライセンスを返します。tryAcquire() メソッドを使用してライセンスの取得を試みることもできます。

スレッド間でデータを交換する Exchanger
Exchanger (exchanger) は、スレッド間で連携するためのツール クラスです。Exchanger は、スレッド間のデータ交換に使用されます。これは、2 つのスレッドが相互にデータを交換できる同期ポイントを提供します。これら 2 つのスレッドは exchange メソッドを介してデータを交換します. 最初のスレッドが exchange() メソッドを最初に実行すると、2 番目のスレッドも exchange メソッドを実行するのを待ちます. 両方のスレッドが同期ポイントに達すると、2 つのスレッドは同期されます. データを交換し、このスレッドによって生成されたデータを相手に渡すことができます。

Exchanger は遺伝的アルゴリズムで使用できます. 遺伝的アルゴリズムでは, 交配対象として 2 人を選択する必要があります. このとき, 2 人のデータが交換され, 交叉規則を使用して 2 つの交配結果が得られます. Exchanger は校正にも使用できます.たとえば、紙の銀行取引明細書を電子銀行取引明細書に手動で入力する必要があります.間違いを避けるために、AB と AB の 2 人が入力するのに慣れています.Excel に入力した後、システムは2 つの Excel をロードし、2 つの Excel データを校正して、それらが一貫して入力されているかどうかを確認する必要があります. コードはコード リストに表示されます.

public class ExchangerTest {
    
    
	private static final Exchanger<String> exgr = new Exchanger<String>();
	private static ExecutorServicethreadPool = Executors.newFixedThreadPool(2);
	public static void main(String[] args) {
    
    
		threadPool.execute(new Runnable() {
    
    
		@Override
		public void run() {
    
    
			try {
    
    
				String A = "银行流水A"; // A录入银行流水数据
				exgr.exchange(A);
			} catch (InterruptedException e) {
    
    
		}
	}
});
	threadPool.execute(new Runnable() {
    
    
	@Override
	public void run() {
    
    
		try {
    
    
			String B = "银行流水B"; // B录入银行流水数据
			String A = exgr.exchange("B");
			System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:"
			+ A + ",B录入是:" + B);
		} catch (InterruptedException e) {
    
    
		}
	}
	});
	threadPool.shutdown();
	}
}

2 つのスレッドのいずれかが exchange() メソッドを実行しない場合、永遠に待機します. 特別な状況が心配な場合は、exchange (V x, longtimeout, TimeUnit 単位) を使用して、最大待機時間を設定できます。

おすすめ

転載: blog.csdn.net/jifashihan/article/details/129851108