[マルチスレッド]-並行プログラミングのパフォーマンスのボトルネック(CPUコンテキストの切り替えとリソースの制限)

序文

「Javaマルチスレッドプログラミングコアテクノロジー」の関連コンテンツがほぼ更新されました。次に、「The Art of Java Concurrent Programming」の貴重な知識ポイントを確認し、電子書籍のリンクを共有します。記事と電子書籍は一緒に学ぶのに適しています。

Java
リンクでの並行プログラミングの技術:https
://pan.baidu.com/s/18H60E_8KDO9uNIuWdghzcg抽出コード:2d8k

1.並行プログラミングのボトルネック

日常の開発では、すべての問題を並列処理で解決できるわけではありません。多くの場合、並列処理の効率はシリアルほど高くない場合があります。たとえば、簡単なデモを作成します。

public class ContextSwitchDemo_01 {
    
    

	public static final long MAX = 10_000;

	// 并行
	public static void concurrent() {
    
    
		try {
    
    
			// 获取系统当前时间
			long currentTimeMillis = System.currentTimeMillis();
			Thread thread = new Thread(() -> {
    
    
				int a = 0;
				for (int i = 0; i < MAX; i++) {
    
    
					a += 5;
				}
			}, "测试线程一");
			thread.start();
			int b = 0;
			for (int i = 0; i < MAX; i++) {
    
    
				b--;
			}
			thread.join();
			long currentTimeMillis2 = System.currentTimeMillis();
			System.out.println("并行方法运行共需时间:" + (currentTimeMillis2 - currentTimeMillis)+"毫秒");
		} catch (InterruptedException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	// 串行
	public static void serial() {
    
    
		long currentTimeMillis = System.currentTimeMillis();
		int a = 0;
		for (int i = 0; i < MAX; i++) {
    
    
			a += 5;
		}
		int b = 0;
		for (int i = 0; i < MAX; i++) {
    
    
			b--;
		}
		long currentTimeMillis2 = System.currentTimeMillis();
		System.out.println("串行方法运行共需时间:" + (currentTimeMillis2 - currentTimeMillis)+"毫秒");
	}
	// 测试入口
	public static void main(String[] args) {
    
    
		concurrent();
		serial();
	}
}

操作の結果を見てみましょう。
ここに画像の説明を挿入
ここでは、2つのスレッドが2つのデータaとbを別々に処理しており、ループの数は同じですが、最終的な実行時間は並列処理よりもシリアル処理の方が短いのですが、これはなぜですか?実際、これは主に2つの理由によるものです。

  1. CPUコンテキストスイッチング
  2. 新しいスレッドの作成によって発生する時間オーバーヘッド(リソース制限)

2、CPUコンテキストの紹介

1.CPUのコンテキストは何ですか

CPUのコンテキストが何であるかを理解するには、最初にCPUの主要コンポーネントが何であるかを理解
ここに画像の説明を挿入
する必要があります。コードで実行するビジネス処理がどれほど複雑であっても、CPUはハードウェアレベルで3つのことしか実行しないことを最初に知る必要があります。 :

  1. メモリまたはキャッシュのデータを読み取る
  2. 読み取ったデータに対して操作を実行します
  3. データをメモリまたはキャッシュに書き込む

複数のプロセスとスレッド処理ビジネスが同時にある場合、私たちの認識ではすべてのビジネスが同時に実行されますが、実際には、CPUのコンピューティングユニットは単位時間あたり1つの命令しか処理できません。CPUが同時に複数のビジネス処理を行っていることを知覚的に感じさせるには、CPUが高速な計算能力を持ち、複数のスレッド命令を絶えず切り替えて、コンピューティングユニットが雨や露に覆われるようにする必要があります。短時間。異なるスレッドで命令を処理すると、ユーザーが同時実行性を認識します。複数のスレッド間の切り替えは、2つのハードウェアから切り離せません。1つは命令を格納するために使用されるレジスタであり、もう1つはCPUが動作している場所を記録するプログラムカウンタです。レジスタとプログラムカウンタは、CPUが命令を処理するために必要なコンテキストになるように一緒に追加されます。

2.コンテキストスイッチ

オペレーティングシステムは、さまざまなシナリオに応じてコンテキストを切り替えるために3つのタイプに分けられます。

  1. プロセスコンテキストスイッチ
  2. スレッドコンテキストスイッチ
  3. 割り込みコンテキストスイッチ

シーンは異なりますが、コンテキスト切り替えの本質は、CPUが現在のタスク処理状態を保存し(コンテキスト情報が保存され)、処理のために新しいタスクをロードする(ロードして新しいコンテキストに切り替える)ことを意味します。

3.コンテキストスイッチングを減らす方法

Javaで使用できる主な方法は3つあります。

  1. 並行プログラミングでロックを使用しないようにしてください。複数のスレッドがロックをめぐって競合すると、コンテキストの切り替えが発生します。スレッドIDを照合するアルゴリズムや、さまざまなデータセグメントのデータを処理するさまざまなスレッドなど、さまざまな方法でロックを使用することを回避できます。
  2. CASアルゴリズム(比較と切り替え):CASアルゴリズムは、共有変数を比較および判断するための方法です。CASアルゴリズムを介してデータを更新すると、ロックの使用を効果的に最適化できます。
  3. 最小数のスレッドを使用してタスクを処理し、冗長スレッドを作成してスレッドのプリエンプションによって引き起こされるコンテキストの切り替えを回避します。
  4. もちろん、コルーチンを使用することもできますが、Java言語自体はコルーチンの実装をサポートしておらず、3部構成のフレームワークを呼び出す必要があります。

後で、CASアルゴリズムを説明するブログを書きます。

3、リソースの制約

1.リソース制限とは何ですか

実際、リソースの制限はよく理解されています。たとえば、自宅の帯域幅オペレーターは1m / sを割り当てます。このとき、知識(zi)と知識(shi)を補足するために、ラブアクションムービーをダウンロードします。時間を統一する100mの映画をダウンロードするのに100秒程度しかかかりませんが、100mの映画を10本同時にダウンロードすると、見るのに1000秒かかるかもしれません。学びたい人にとっては絶対に耐えられません。 。オペレーティングシステムとの類似性は、タスクを処理するときに、客観的な要因(帯域幅、ハードディスクの読み取りと書き込みの速度、CPUの処理速度など)の影響を受けることを意味します。タスクを処理するときは、次のことを行う必要があります。タスクを完了するために、これらの客観的な要因を考慮してください。

2.リソースの制約を回避する場合

リソースの制約が何であるかを理解すれば、実際には非常に簡単に解決できます。

  1. 最初のポイントは間違いなくお金を追加することです!
    クラスターの構築や帯域幅の増加など、客観的要因の制約を解決することで、リソース制約の発生を回避できます。客観的要因の制約領域が増えると、リソース制約のボトルネックもある程度解決されます。
  2. お金がない場合はどうなりますか?
    私たちの会社は確かに問題を解決するためにお金を燃やすことができない場合がありますが、現時点では、リソースの再利用の問題を考慮する必要があります。たとえば、スレッドプールを介してスレッドを作成することによって引き起こされる過度のリソースオーバーヘッドを回避できます。

4、デッドロック

上記の問題がすべてソフトウェアレベルにある場合、デッドロックの問題は間違いなくハードウェアレベルの人的要因によって引き起こされます。最初にデッドロックを実装するだけです。

public class DeadLockDemo_01 {
    
    

	private Object lockA = new Object();
	private Object lockB = new Object();

	public void lockA() throws InterruptedException {
    
    
		System.out.println("lockA方法运行!");
		synchronized (lockA) {
    
    
			Thread.sleep(2_000);
			synchronized (lockB) {
    
    
			}
		}
		System.out.println("lockA方法结束!");
	}

	public void deadLock() throws InterruptedException {
    
    
		System.out.println("deadLock方法运行!");
		Thread.sleep(1_000);
		synchronized (lockB) {
    
    
			synchronized (lockA) {
    
    
			}
		}
		System.out.println("deadLock方法结束!");
	}

	public static void main(String[] args) throws InterruptedException {
    
    
		DeadLockDemo_01 deadLockDemo_01 = new DeadLockDemo_01();

		new Thread(() -> {
    
    
			try {
    
    
				deadLockDemo_01.lockA();
			} catch (InterruptedException e) {
    
    
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}).start();

		new Thread(() -> {
    
    
			try {
    
    
				deadLockDemo_01.deadLock();
			} catch (InterruptedException e) {
    
    
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}).start();

	}
}

操作結果:
ここに画像の説明を挿入
ここで、デッドロックは、2つの方法が互いにロックを解除するのを待っているために発生します。この現象は、本番環境では壊滅的です。デッドロックを回避するには、次のことを確認する必要があります。

  1. 同じスレッドで複数のロックを保持することは避けてください
  2. 同じスレッドが1つのロックで複数のリソースをスケジュールすることは避けてください
  3. より多くのタイムロックとスニッフィングロックを使用できます
  4. データベース接続の場合、lockコマンドとunlockコマンドが同じ接続によって発行されていることを確認してください

この時点で、今日のコンテンツは終了しました。習得した学生がそれを気に入って、お気に入りを追加して、作成者にコードワードを継続するように促すことを願っています。
幸運を!

おすすめ

転載: blog.csdn.net/xiaoai1994/article/details/111215620