目次
4. マルチスレッドによってもたらされるリスク - スレッドの安全性 (最も重要)
4. 読み取り/書き込みロック VS ミューテックス ロック
10. JUCの共通クラス(java.util.concurrent)
1. スレッドを理解する (スレッド)
1.コンセプト
マルチプロセスは、並行プログラミングの効果をすでに十分に発揮しています。
ただし、プロセスが重すぎるという明らかな欠点があります。
1. より多くのリソースを消費する
2. 速度が遅い
プロセスの作成と破棄が頻繁に行われない場合は問題ありませんが、大規模なプロセスの作成と破棄が必要になると、オーバーヘッドが比較的高くなります。
オーバーヘッドは主に、リソースをプロセスに割り当てるプロセスに反映されます。
そこで、賢いプログラマは方法を考えました。プロセスを作成するときに、後続のメモリやハードディスクのリソースを割り当てるのではなく、単純な PCB だけを割り当てることができるでしょうか?
したがって、軽量プロセス、つまりスレッド(Thread)が存在します。
スレッドは PCB を作成するだけで、メモリやハードディスクなどの後続のリソースは割り当てません。
スレッドは一部のタスクを実行するために作成されますが、タスクを実行するにはこれらのハードウェア リソースを消費する必要があります。
したがって、作成するものは依然としてプロセスですが、プロセスを作成すると、すべてのリソースが割り当てられ、その後に作成されるスレッドはプロセス内に保持されます(プロセスとスレッドの関係は、スレッドを含むプロセスと考えることができます)。
後続のプロセスの新しいスレッドは、前のプロセスで作成されたリソースを直接再利用します。
実際、プロセスには少なくとも 1 つのスレッドが含まれている必要があります
したがって、最初に作成されたものは、スレッドを 1 つだけ含むプロセスとみなすことができます (このときの作成プロセスにはリソースを割り当てる必要があり、最初のスレッドの作成オーバーヘッドはこの時点で比較的大きい可能性があります)。
ただし、後からこの処理でスレッドを作成すると、リソースはすでに存在するため、リソースを割り当てる処理を省略できます。
プロセス内の複数のスレッドはプロセス内のさまざまなリソース (メモリ、ハードディスク) を共同で再利用しますが、これらのスレッドは CPU 上で個別にスケジュールされます。
したがって、スレッドは「同時プログラミング」の効果を実現できるだけでなく、比較的軽量な方法で実行することもできます。
ねじ山も PCB を介して記述されます
Windows では、プロセスとスレッドは異なる構造によって記述されますが、Linux では、Linux 開発者はスレッドを記述するために PCB 構造を再利用します。
このとき、1 つの PCB が 1 つのスレッドに対応し、複数の PCB が 1 つのプロセスに対応します。
PCB 内のメモリ ポインタとファイル記述子テーブル。同じプロセスの複数の PCB では、これら 2 つのフィールドの内容は同じですが、コンテキスト ステータス、優先順位、アカウンティング情報...サポート スケジューリング属性。これらの各 PCB は違う
したがって、次の 2 つの文があります。
プロセスは、オペレーティング システムによる割り当ての基本単位です。
スレッドは、オペレーティング システムによるスケジューリングと実行の基本単位です。
マルチプロセス プログラミングと比較すると、マルチスレッド プログラミングには、より軽量で、作成と破棄がより高速であるという利点があります。
しかし、プロセスほど安定していないという欠点もあります。
Java では、並行プログラミングを行う場合でも、マルチスレッドを考慮する必要があります。
「マルチスレッド」も「マルチプロセス」も、本質的には「並行プログラミング」の実装モデルですが、実は「並行プログラミング」の実装モデルは他にもたくさんあります。
プロセスとスレッドの違いとつながりについて話す
1. プロセスにはスレッドが含まれますが、これはすべて「同時プログラミング」を実現するためであり、スレッドはプロセスよりも軽量です。
2. プロセスはシステムによるリソース割り当ての基本単位であり、スレッドはシステムのスケジューリングと実行の基本単位です。プロセスの作成時に、リソースの割り当て作業が行われます。後でスレッドを作成すると、次のことができます。以前のリソースを直接共有します。
3. プロセスは独立したアドレス空間を持ち、相互に影響を与えない プロセスの独立性によりシステムの安定性が向上する 複数のスレッドがアドレス空間を共有する スレッドが例外をスローすると、プロセス全体が異常終了する可能性がある複数のスレッドが互いに簡単に影響を与える可能性があることを意味します。
4. 他にもいくつかポイントがありますが、上記の 3 つの核心ポイントは非常に重要です。
スレッドは軽量ですが、作成コストがないわけではなく、スレッドの作成と破棄が頻繁に行われる場合、オーバーヘッドは無視できません。
このとき、賢明なプログラマはさらに 2 つの方法を考えました。
1.「軽量スレッド」、つまりコルーチン/ファイバー
これはまだ Java 標準ライブラリに組み込まれていませんが、コルーチンを実装するサードパーティ ライブラリがいくつかあります。
2.「スレッドプール」
プール (投票) は、実際にはコンピューターにおける非常に古典的な考え方です。リソースを急いで解放するのではなく、後で使用するためにプールに入れます。リソースを申請するときは、リソースを申請する必要もあります。リソースを申請して「プール」に入れるのは簡単で、その後の申請がより便利になります。
2. 最初のマルチスレッドプログラム
スレッド自体はオペレーティング システムによって提供される概念であり、オペレーティング システムはプログラマが使用できるいくつかの API (Linux、pthread) も提供します。
JavaではOSのAPIをカプセル化してスレッドクラスを提供します。
まずクラスを作成し、Threadを継承させて、runメソッドを書き換えますここでのrunはスレッドのentryメソッドに相当します スレッドの実行開始後に何をするかをこのrunで記述します
クラスを作成するだけでなく、それを呼び出して実行する必要もあります。ここでの開始はスレッドの作成です (この操作では、下部でオペレーティング システムが提供する API を呼び出し、同時にスレッドを作成します)オペレーティング システム カーネル内のスレッド。対応する PCB 構造および対応するリンク リストに追加されます)
このとき、新しく作成されたスレッドは CPU スケジューリングに参加します。このスレッドが実行する次の作業は、上で書き直した run メソッドです。
java.lang は特別なパッケージであり、ここにあるクラスは手動でインポートする必要はなく、デフォルトで使用できます。
class Mythread extends Thread{
@Override
public void run() {
System.out.println("hello world!");
}
}
public class Demo1 {
public static void main(String[] args) {
Mythread mythread = new Mythread();
mythread.start();
}
}
つまり、クリックしてプログラムを実行すると、最初に Java プロセスが作成されます。このプロセスには少なくとも 1 つのスレッドが含まれます。このスレッドはメイン スレッドとも呼ばれ、メイン メソッドの実行を担当するスレッドです。
上記のコードを mythread.run(); に変更すると、これも正しく実行できることがわかりますが、start とは異なります。
run は上記のエントリ メソッド (通常のメソッド) にすぎず、システム API を呼び出したり、実際のスレッドを作成したりしません。
次に、コードを次のように調整します。
class Mythread extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello thread!");
}
}
}
public class Demo1 {
public static void main(String[] args) {
Mythread mythread = new Mythread();
mythread.start();
//mythread.run();
while(true){
System.out.println("hello main!");
}
}
}
このとき、mainメソッド内にwhileループがあり、スレッドの実行中にもwhileループがあり、どちらもwhile(ture)無限ループになります。
start メソッドを使用して実行します。このとき、両方のループが交互に実行され、結果が出力されます。これは、2 つのスレッドがそれぞれ独自のループを実行するためです。両方のスレッドは CPU のスケジューリングに参加できます。これら 2 つのスレッドは同時に実行されます。
メイン スレッドと新しいスレッド スタイルの同時実行の関係は、オペレーティング システムがそれをスケジュールする方法によって異なります。
実行モードで書かれた場合、新しいスレッドは作成されておらず、元のメインスレッド内にあることを意味します。while in run が実行された後でのみ、呼び出し元の場所に戻って後で実行できます。 while in run これは無限ループであるため、メインスレッド内の while は実行される機会がありません。
各スレッドは独立した実行フローです
各スレッドはコードの一部を実行できます
各スレッド間には同時実行関係があります
場合によっては、実行するためにスリープを追加します
Thread.sleep(1000);
これらの違いを Java で統一しました。Thread.sleep は、上記のシステム関数をカプセル化することに相当します。Windows バージョンの jvm で実行されている場合、最下層は Windows の Sleep を呼び出します。Linux バージョンの jvm で実行されている場合、最下層は Windows の Sleep を呼び出します。 jvm、最下層は Linux スリープを呼び出すことです
sleep は Thread クラスの静的メソッドです
この例外については、現時点では try-catch メソッドを使用して解決することを選択しており、再度実行すると、リズミカルな実行になります。
注: これは厳密な変更ではなく、順序が逆になる場合もあります。
これは、2 つのスレッドがスリープ後にブロッキング状態になるためです。時間が経過すると、システムは 2 つのスレッドをウェイクアップし、2 つのスレッドのスケジューリングを再開します。両方のスレッドがウェイクアップすると、スケジューリングの順序が考慮されます。 「ランダム」として
システムが複数のスレッドをスケジュールするとき、明確な順序はありませんが、この「ランダム」な方法でスレッドをスケジュールします。この「ランダム」なスケジューリング プロセスは「プリエンプティブ実行」と呼ばれます。
私たちが通常話しているランダム性は、実際には「等しい確率」の設定を意味しますが、ここでのランダム性は少なくともランダムに見えますが、実際には必ずしも等しいプロファイルを保証するものではありません。
(1) スレッドを観察する
スレッドを作成した後、いくつかの方法でそれを直接観察することもできます。
1. idea のデバッガーを直接使用する
実行する場合は、デバッグ方法に従って実行することを選択します。
2、jコンソール
jdkで提供される公式デバッグツール
以前にダウンロードした jdk のパスに従って、bin ディレクトリで jconsole を見つけることができます。
次に、最初にプログラムを実行してから jconsole を開きます。これには、システム上で実行されているすべての Java プロジェクトがリストされます (jconsole も Java で書かれたプログラムです)。
然后选中线程在进行连接,然后选择线程标记页
此时,左下角就显示了所有的线程这里列出了当前进程中所有的线程,不仅仅是主线程和自己创建的线程,还有一些别的线程,其它剩下的线程,都是 JVM 里面自带的,负责完成一些其它方面的工作
点击线程之后,就可以查看到线程的详细信息
堆栈跟踪:线程的调用栈、显示方法之间的调用关系
当程序卡死了,我们就可以查看一下这里每个线程的调用栈,就可以大概知道是在哪个代码中出现的卡死的情况
3、创建线程
Java中,通过Thread 类创建线程的方式,还有很多种写法
1、创建一个类,继承 Thread ,重写 run 方法
2、创建一个类,实现 Runnable,重写 run 方法
实现 指实现 interface
Java中,interface 通常会使用一个形容词词性的词来描述
package thread;
import java.util.TreeMap;
class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
//使用 Runnable 的方式创建线程
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
区别:
Thread 这里,是直接把要完成的工作放到了 Thread 的 run 方法中
Runnable 这里,则是分开了,把要完成的工作放到了 Runnable 中,再让 Runnable 和 Thread 配合
把线程要执行的任务,和线程本身,进一步的解耦合了
3、继承Thread,重写 run ,基于匿名内部类
1、先创建了一个子类,这个子类继承自Thread,但是这个子类没有名字(匿名),另一方面,这个类的创建是在Demo3里面
2、在子类中,又重写了run方法
3、创建了该子类的实例,并且使用 t 这个引用来指向
//通过匿名内部类的方式,创建线程
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
@Override
public void run() {
while(true){
System.out.println("hello Thead!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
while(true){
System.out.println("hello main!");
Thread.sleep(1000);
}
}
}
4、实现Runnable,重写run,基于匿名内部类
1、创建了一个Runnable子类(类,实现Runnable)
2、重写了run方法
3、把子类创建出实例,把实例传给Thread的构造方法
)对应的是Thread 构造方法的结束,new Runnable 整个一段,都是在构造出一个Thread的参数
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t.start();
while(true){
System.out.println("hello main!");
Thread.sleep(1000);
}
}
}
5、使用lambda 表达式,表示run的内容(推荐)
lambda表达式本质上就是一个“匿名函数”
このような匿名関数は主にコールバック関数として使用できます。
コールバック関数はプログラマが積極的に呼び出す必要はなく、適切なタイミングで自動的に呼び出されます。
「コールバック関数」がよく使用されるシナリオ:
1. サーバー開発: サーバーはリクエストを受信し、対応するコールバック関数をトリガーします。
2. グラフィカル インターフェイスの開発: 特定のユーザー操作により、対応するコールバックがトリガーされます。
ここでのコールバック関数は、スレッドが正常に作成された後に実際に実行されます。
lambda と同様、基本的に新しい言語機能はありませんが、これまでに実装できた関数がより簡潔に記述されています。
public class Demo5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while(true){
System.out.println("hello Thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
スレッドの作成方法には上記の5つ以外にもCallable や thread pool を利用した方法もあり、現在ではこれらが一般的な記述方法となっています。
2. スレッドクラスと共通メソッド
Thread は Java スレッドのスポークスマンです。つまり、システム内のスレッドは Java の Thread オブジェクトに対応します。スレッドに関するさまざまな操作は Thread を通じて実行されます。
1. スレッドの一般的な構築方法
Java では、スレッドに「Thread-0」という名前を付けますが、これは読みにくくなります。プログラム内にさまざまな機能を持つスレッドが数十個ある場合は、スレッドの作成時にスレッドに名前を付けることができます。
public class Demo6 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while(true){
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"myThread");
t.start();
}
}
ここではスレッドに「Mythread」という名前を付けます。コードが実行されると、jconsole でスレッドを見つけることができます。
この時点では、main の実行が終了しているため、スレッドに main が存在しないことがわかります。
start によってスレッドが作成され、スレッド エントリ メソッドが実行されます (メイン スレッドの場合は main、その他のスレッドの場合は run / lambda)
2. スレッドのいくつかの共通属性
ID:getId()
スレッドの ID (JVM 内のスレッドに設定された ID)
人は多くの名前を持つことができ、スレッドも複数の ID を持つことができます。
JVM には ID、pthread ライブラリ (システムがプログラマに提供するオペレーティング システムの API)、および ID があります。カーネルでは、スレッド用の PCB にも ID があります。これらの ID は互いに独立しています。
ステータス、優先度:
Java とオペレーティング システムのスレッド ステータスには特定の違いがあります。
スレッドのスケジューリングは主にシステム カーネルが担当するため、優先度の設定/取得はあまり役に立ちません。
バックグラウンド スレッドかどうか:
バックグラウンド スレッド/デーモン スレッド: バックグラウンド スレッドは終了に影響しません。
フォアグラウンド スレッド: プロセスの終了に影響します。フォアグラウンド スレッドの実行が完了していない場合、プロセスは終了しません。
プロセス内のすべてのフォアグラウンド スレッドが実行を完了すると、バックグラウンド スレッドの実行が完了していなくても、バックグラウンド スレッドはプロセスとともに終了します。
作成されるスレッドはデフォルトではフォアグラウンド スレッドですが、setDeamon() を通じてバックグラウンド プロセスを設定できます。
//后台线程 和 前台线程
public class Demo7 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while(true){
System.out.println("hello Thread!");
}
});
//设置成后台线程
t.setDaemon(true);
t.start();
}
}
生きているのか?
(システムカーネル内の) Thread オブジェクトに対応するスレッドが生きているかどうかを示します。
言い換えれば、Thread オブジェクトのライフサイクルは、システム内のスレッドと完全には一致していません。!!
一般に、Thread オブジェクトが最初に作成され、カーネルが実際にスレッドを作成する前に start が手動で呼び出されます。
終了すると、Thread オブジェクトが最初にライフサイクルを終了する可能性があります (このオブジェクトは参照されません)。
Thread オブジェクトがまだ存在しており、カーネル内のスレッドが最初に実行を完了してから終了する可能性もあります。
3. スレッドを開始します: start
システム内で、実際にスレッドを作成します。
1. PCB を作成する
2. PCB を対応するリンク リストに追加します。
これはシステム カーネルによって行われます
オペレーティング システムのカーネルとは何ですか?
オペレーティング システム = カーネル + サポート プログラム
カーネルには、システムのコア機能が含まれています。
1. そうですね、さまざまなハードウェアデバイスを管理します
2. 正解です。さまざまなプログラマーに安定した動作環境を提供します。
最も重要なことは、システムの API を呼び出してスレッドの作成作業を完了することです。
start メソッド自体の実行は一瞬で完了するため、start 呼び出し後はすぐに後続の start ロジックの実行を続けます。
4. スレッドを終了する
run メソッドが実行された後、スレッドは終了します。
ここでの終了とは、できるだけ早く実行を完了する方法を見つけることです。
通常、実行が完了する前にスレッドが突然消えることはありませんが、スレッドが作業の半分を完了して突然消えると、問題が発生することがあります。
(1) プログラマが手動でフラグを設定する
この手動で設定されたフラグを通じて
//线程终止
public class Demo8 {
public static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(!isQuit){
System.out.println("hello Thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
//主线程这里执行一些其它逻辑之后,让 t 线程结束
Thread.sleep(3000);
//这个代码就是在修改前面设置的标志位
isQuit = true;
System.out.println("把线程终止");
}
}
上記のコードにより、t スレッドは実際に終了し、sleep(3000) が実際に 4 回出力されますが、これは正常です。
主な理由はスリープ動作のエラーで、実際のスリープ時間が 3000 を超えると、t スレッドが 4 番目のログを出力する場合があるため、3 回または 4 回の出力が可能になります。
isQuit はメンバー変数として記述するとアクセスできるのに、ローカル変数として記述するとエラーが報告されるのはなぜですか?
ラムダ式は外部変数をキャプチャできます
ラムダ式の実行時間が遅いので、後で実際にラムダが実行されるときにローカル変数isQuitが破棄されている可能性が出てきます。
つまり、メインスレッドが先に終了し、この時点で isQuit が破棄されるのですが、実際にラムダが実行される時点では isQuit は破棄されています。
この状況は客観的に存在しており、ラムダが破棄されていない変数にアクセスできるようにするのは明らかに不適切です。
lambdaでは「変数キャプチャ」などの仕組みを導入
ラムダは内部で外部変数に直接アクセスしているように見えますが、実際には本質的に外部変数をラムダにコピーしています (これにより、先ほどのライフサイクルの問題が解決できます)
ただし、変数キャプチャには制限があります
変数キャプチャでは、キャプチャされた変数が最終的なものである必要があります
isQuit = true; を削除すると、エラーは発生しませんが、プロンプトが表示されます。
これは、最終的な変更が使用されていないにもかかわらず、コード内で変数が変更されていないことを意味し、このとき、変数は最終的な変数であるとみなすこともできます。
この変数を変更したい場合、現時点では変数キャプチャを実行できません。
では、なぜこれを設定する必要があるのでしょうか?
Java は をコピーすることで「変数キャプチャ」を実装します。外部コードがこの変数を変更したい場合、外部変数は変更されたが内部変数は変更されていないという状況が発生します (曖昧さが発生しやすい)
対照的に、他の言語 (JS) はより急進的な設計を採用しています。変数キャプチャもあります。これはコピーによって実装されるのではなく、外部変数のライフサイクルを直接変更するため、ラムダが実行時に確実に外部にアクセスできるようになります。 . 変数 (現時点では、js での変数キャプチャには最終的な制限はありません)
メンバ変数を書く場合は「変数のキャプチャ」を引き起こす仕組みではなく、「内部クラスが外部クラスのメンバにアクセスする」という仕組みなので、それ自体はOKであり、finalかどうかを気にする必要はありません。
(2) ダイレクトスレッドクラス
Thread クラスは既製のフラグを提供するため、フラグを手動で設定する必要はありません。
//线程终止,使用 Thread 自带的标志位
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
//Thread.currentThread() 其实就是 t
//但是 lambda 表达式是在构造 t 之前就定义好了的,编译器看到的 lambda 里的 t 就会认为这是一个还没初始化的对象
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello Thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(3000);
//把上述的标志位设置成 true
t.interrupt();
}
}
コードを実行すると、プログラムが私たちの考えどおりに実行されないことがわかります。
プロンプトによると、次のように理解できます: t スレッドはスリープ状態で、割り込みによって目覚めます。
フラグ ビットが手動で設定されている場合、現時点ではスリープを解除できません。
スレッドは通常に実行されているか、スリープしている可能性があります。このスレッドがスリープしている場合は、起動する必要がありますか?
まだ目覚める必要がある
したがって、スレッドがスリープ状態で、他のスレッドが割り込みメソッドを呼び出した場合、スリープは強制的に例外をスローし、スリープはすぐに解除されます (スリープ (1000) を設定したと仮定します。ただし、10 ミリ秒しか経過していませんが、1000 ミリ秒まではいいえ) 、それもすぐに起こされます)
ただし、スリープが解除されると、以前に設定されたフラグは自動的にクリアされます。!!
この時点で、スレッドを終了させ続けたい場合は、キャッチにブレークを追加するだけです。
//线程终止,使用 Thread 自带的标志位
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
//Thread.currentThread() 其实就是 t
//但是 lambda 表达式是在构造 t 之前就定义好了的,编译器看到的 lambda 里的 t 就会认为这是一个还没初始化的对象
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello Thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//throw new RuntimeException(e);
break;
}
}
});
t.start();
Thread.sleep(3000);
//把上述的标志位设置成 true
t.interrupt();
}
}
スリープが解除された後、プログラマは次の方法で作業を進めることができます。
1. ループを直ちに停止し、スレッドを直ちに終了します。
2. 他の処理を継続し、しばらくしてからスレッドを終了します (catch で他のロジックを実行し、実行後にブレークします)。
3. 終了したリクエストを無視してループを継続します (break は書き込まないでください)。
前者は静的メソッドであり、判定と同時に判定フラグがクリアされます。
後者はメンバーメソッドであるため、後者を使用することをお勧めします。
5. スレッドを待つ
複数のスレッドが同時に実行され、特定の実行プロセスがオペレーティング システムによってスケジュールされます。
オペレーティング システムのスケジューリング プロセスは「ランダム」です
したがって、スレッドが実行される順序を決定することはできません。
スレッドを待つことは、スレッドが終了する順序を計画する手段です。
終了待ちとは、runメソッドの実行が完了するまで待つことです。
ブロッキング: コードを一時的に実行し続けないようにします (スレッドは当面、CPU でのスケジューリングに参加しません)。
スレッドはスリープによってブロックすることもできますが、このブロックには時間制限があります。
結合のブロックは「死ぬまで待つ」ことになります
割り込みによって結合を解除できますか?
はい、スリープ、参加、待機... ブロックした後、割り込みメソッドによって目覚めることができます。これらのメソッドは目覚めた後にフラグ ビットを自動的にクリアします (スリープと同様)
public class Demo10 {
public static void main(String[] args) {
Thread b = new Thread(() -> {
for(int i = 0;i < 5;i ++){
System.out.println("hello B!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("b 结束了");
});
Thread a = new Thread(() -> {
for(int i = 0;i < 3;i ++){
System.out.println("hello A!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
try {
//如果 b 此时还没执行完毕, b.join 就会产生阻塞情况
b.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("A 结束了");
});
b.start();
a.start();
}
}
6. 現在のスレッドの参照を取得します。
私たちはこの方法によく慣れています
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
7. 現在のスレッドをスリープします
パラメータの単位はmsです
しかし、睡眠自体には特定のエラーがあります
sleep(1000) を設定しますが、必ずしも正確に 1000 ミリ秒スリープすることを意味するわけではありません。!(スレッドのスケジューリングにも時間がかかります)
sleep(1000) は、スレッドが 1000 ミリ秒後に「準備完了状態」に戻ることを意味します。このとき、CPU 上でいつでも実行できますが、すぐには指定できない場合があります。
スリープの特性により、特別なテクニックが誕生しました: sleep(0)
現在のスレッドに CPU を放棄させ、次のラウンドのスケジューリングを準備させます (通常、バックグラウンド開発には関与しません)。
yield メソッドの効果は sleep(0) の効果と同じです。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
3. スレッドのステータス
1. スレッドのすべての状態を観察する
Java では、合計で次の 6 つの状態が提供されます。
新しい
Thread オブジェクトは作成されましたが、Thread メソッドはまだ呼び出されていません。
これはコードで確認できます。
//线程的状态
public class Demo11 {
public static void main(String[] args) {
Thread t = new Thread(() ->{
while(true){
// System.out.println("hello Thread!");
}
});
System.out.println(t.getState());
t.start();
}
}
実行可能
準備完了状態は、次の 2 つの状況として理解できます。
(1) CPU上でスレッドが実行されている
(2) スレッドはここでキューに入れられ、いつでも CPU 上で実行できます。
//线程的状态
public class Demo11 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
while(true){
}
});
System.out.println(t.getState());
t.start();
//t.join();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
ブロックされました
ロックはブロックを作成するため、
待っている
waitが呼び出されるためにブロッキングが発生します。
TIMED_WAITING
睡眠がブロックされる から
タイムアウトありの結合バージョンを使用すると、TIMED_WAITING も生成されます。
//线程的状态
public class Demo11 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
System.out.println(t.getState());
t.start();
//t.join();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
終了しました
作業完了ステータス
//线程的状态
public class Demo11 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
System.out.println(t.getState());
t.start();
t.join();
System.out.println(t.getState());
}
}
2. スレッドステータスとステータス転送の意味
よりシンプルで理解しやすい方法を使用して状態間の遷移を理解する場合は、次の図を使用してそれを表すことができます。
4. マルチスレッドによってもたらされるリスク - スレッドの安全性 (最も重要)
1. スレッドの安全性の問題のデモンストレーション
まずこのコードとその結果を見てみましょう。
//线程安全问题演示
class Counter{
public int count = 0;
public void increase(){
count++;
}
}
public class Demo12 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() ->{
for(int i = 0;i < 50000;i++){
counter.increase();
}
});
Thread t2 = new Thread(() ->{
for(int i = 0;i < 50000;i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
論理的に言えば、このときの実行結果は 100000 になるはずですが、次のような結果が表示されました。
論理的に言えば、2 つのスレッドが変数の循環自己インクリメントを実行し、それぞれが変数を 50,000 回インクリメントします。期待される結果は 10,000,000 であるはずですが、実際はそうではありません。プログラムを複数回実行すると、次の結果が得られることがわかります。それぞれの操作は異なります。同じものはありません
マルチスレッドでは、マルチスレッドの実行によって引き起こされるバグを総称して「スレッド セーフの問題」と呼ぶことがわかりました。特定のコードが単一スレッドまたは複数のスレッドで実行しても問題がない場合、それは「スレッド セーフ」と呼ばれます。その逆は「スレッドが安全ではない」と呼ばれます
したがって、スレッドが安全かどうかは、コードにバグがあるかどうか、特にマルチスレッドによって引き起こされるバグがあるかどうかによって決まります。
count++;
この操作は基本的に次の 3 つのステップで構成されます。
1. メモリ上のデータを CPU レジスタにロードします (ロード)。
2. レジスタのデータに+1を加算します(add)
3. レジスタ内のデータをメモリ (sava) に書き込みます。
上記の操作を 2 つ以上のスレッドで同時に実行すると、問題が発生する可能性があります。
このカウントの実行プロセスをよりよく表現するために、タイムラインを描画して理解を深めます。
ここでは、これら 2 つのスレッドのスケジュール順序が不確実であるため、これら 2 つの操作セットの相対的な順序にも違いが生じます。
2 回インクリメントされますが、2 つのスレッドが同時に実行されるため、実行順序によっては演算の中間結果が上書きされる可能性があります。
この 50,000 サイクルの間に、2 つのスレッドの ++ は何回連続し、上書き結果は何回表示されますか? スレッドのスケジューリングは「ランダム」であるため、不確実です。
ここでの結果は問題 i を引き起こし、得られる誤差値は 10w 未満でなければなりません。
count++ だけでなく、多くのコードにはスレッド セーフティの問題が含まれています。
2. スレッドの安全性の問題の原因
1. [根本原因] 複数のスレッド間のスケジューリング順序は「ランダム」であり、オペレーティング システムは「プリエンプティブ」実行戦略を使用してスレッドをスケジュールします。
シングルスレッドとは異なり、マルチスレッドではコードの実行順序により多くの変化が生じますが、これまではコードが固定された順序で実行され、正しく実行されることだけを考慮する必要がありました。ここで、マルチスレッド下、N 個の実行順序の下では、コードの実行結果は正しくなければならないことを考慮する必要があります。
現在主流のオペレーティング システムはすべて、この種のプリエンプティブ実行を使用しています。
2. 複数のスレッドが同じ変数を同時に変更するため、スレッドの安全性の問題が簡単に発生する可能性があります。
1 つのスレッドが変数を変更する、複数のスレッドが同じ変数を読み取る、複数のスレッドが複数の変数を変更する、この 3 つの状況はすべて問題ありません。
この状況は実際にはコード構造の問題を表しています。コード構造を調整することでこの状況を回避できます。これはスレッド セーフティの問題を解決する最も重要な方法でもあります。
3. 行われた変更は「アトミック」ではありません
変更操作をアトミックな方法で完了できる場合、現時点ではスレッドの安全性の問題は発生しません。
4. メモリの可視性によって引き起こされるスレッドの安全性の問題
5. 命令の並べ替えによって生じるスレッド セーフティの問題
3. 前のスレッドのセキュリティの問題を解決する
ロックは、一連の操作を「アトミック」操作にパッケージ化することと同じです。
トランザクションのアトミック操作は主にロールバックに依存しており、ここでのアトミック操作はロックを通じて「相互に排他的」です。つまり、自分のスレッドが動作しているときは、他のスレッドは動作できません。
コード内のロックにより、複数のスレッドがこの変数を同時に使用できるようになります。
前のコードに戻ると、++ のカウントが実行されるときにロック操作が実行されます。
Javaではsynchronizedキーワードが導入されています
メソッドをロックするとは、メソッドに入るときはロック(ロック)され、 メソッドから出るときはロックが解除(アンロック)されることを意味します。
t1 がロックした後、t2 もロックを試みます。このとき、t2 はブロックして待機します。このブロックは、t1 がロックを解放し、t2 が正常にロックできるまで継続します。
ここでの t2 のブロッキング待機により、t2 の count に対する ++ 操作が後まで延期されます。t1 が count++ を完了した場合にのみ、t2 が実際に count++ を実行できるようになり、「インターリーブ実行」が「シリアル実行」に変わります。
そこで今回は、ロック後の効果を見てみましょう。
ロック操作により同時実行がシリアル実行に変わるわけですが、現時点でもマルチスレッドの意味はあるのでしょうか?
現在のスレッドは count++ を実行するだけではありません。ロックのため、増加メソッドはシリアルになります。ただし、上記の for ループはスレッド セーフティの問題を含まないため、ロックされません。for ループ内で操作される変数 i はローカル変数です。スタック上にあります。2 つのスレッドには 2 つの独立したスタック スペースがあり、これらは完全に異なる変数です。
つまり、2 つのスレッドの i は同じ変数ではなく、2 つのスレッドが 2 つの異なる変数を変更しても、スレッドの安全性の問題は発生せず、ロックする必要もありません。
したがって、これら 2 つのスレッドでは、コードの一部はシリアルに実行され、一部は同時に実行されますが、それでも純粋なシリアル実行よりも効率的です。
4. 同期キーワード
このキーワードはJavaが提供するロック方式(キーワード)です。
これは多くの場合、コード ブロックを使用して行われます。
1. コードブロック入力時にロックする
2. コードブロックを抜けた後にロックを解除する
同期ロックとロック解除は、実際には「オブジェクト」次元で拡張されます。
ロックの目的は、相互排他のためのリソース (相互排他的な変更変数) を使用することです。
synchronizedを使用する場合、実際には特定のオブジェクトを指定してロックすることになりますが、synchronizedがメソッドを直接変更する場合はこれをロックするのと同等になります(変更したメソッドは上記のコードを簡略化したものに相当します)
2 つのスレッドが同じオブジェクトをロックすると、ロック競合/ロック競合が発生します (1 つのスレッドは正常にロックできますが、もう 1 つのスレッドはブロックされて待機します)。
2 つのスレッドが異なるオブジェクトをロックする場合、ロックの競合は発生せず、ブロックや待機、一連の操作は発生しません。
先ほどのコードのように、2 つのスレッドが異なるオブジェクトをロックする場合、ブロック待機は発生せず、2 つのスレッドは count++ をシリアルに実行することはできませんが、スレッドの安全性は維持されます。
たとえば、次のコード:
//线程安全问题演示
class Counter{
public int count = 0;
public Object locker = new Object();
public void increase(){
synchronized (this){
count++;
}
}
public void increase2(){
synchronized (locker){
count++;
}
}
}
public class Demo12 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() ->{
for(int i = 0;i < 50000;i++){
counter.increase();
}
});
Thread t2 = new Thread(() ->{
for(int i = 0;i < 50000;i++){
counter.increase2();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
しかし、最初に増加したロックオブジェクトをロッカーに変更すると、この時点では同じオブジェクトのままとなり、ブロック待ちが発生するため、スレッドセーフティの問題は自然に解決されます。
どのオブジェクトがロックされているかは問題ではありません。最も重要なことは、2 つのスレッドが同じオブジェクトをロックしているかどうかです。
次のコードで、一方のスレッドがロックされ、もう一方のスレッドがロックされていない場合、この時点でスレッドの安全性の問題は発生しますか?
まだ問題があります! !!
顔を平手打ちしても音は鳴りません。一方的なロックはロックがないことを意味します。複数のスレッドが同じオブジェクトをロックするのは当然のことです。
どのオブジェクトをロックするかは関係ありません () 内に記述する必要はありません 何を書いても問題ありません 重要なのは、複数のスレッドのロック操作が同じオブジェクトに対するものであるかどうかです 書かれたオブジェクトin synchronized は 1 つである可能性がありますが、実際には操作のメンバーは別のメンバーである可能性があります
synchronized を使用して静的メソッドを変更する場合、それは synchronized を使用してクラス オブジェクトをロックするのと同じです。
クラスの完全な情報は、最初は .java ファイルにあり、さらに .class にコンパイルされます。jvm が .class をロードすると、内容が解析され、メモリ内にオブジェクト (クラスのクラス オブジェクト) が構築されます。 。
synchronized では、() にクラス オブジェクトを書くか、通常のオブジェクトを書くかは関係ありません。synchronized では、オブジェクトが何であるかは考慮されず、2 つのスレッドが同じオブジェクトをロックしているかどうかのみが考慮されます。
5. メモリの可視性によって引き起こされる問題
これで、次のコードが完成しました。
t1 は常に while ループを実行し、t2 ではユーザーがコンソールを介して isQuit の値として整数を入力できるようにします。
ユーザー入力が 0 のままの場合、t1 スレッドは実行を継続します。ユーザー入力が 0 以外の場合、スレッドはループを終了する必要があります。
//内存可见性
import java.util.Scanner;
public class Demo13 {
public static int isQuit = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
while(isQuit == 0){
}
System.out.println("t1 执行结束");
});
Thread t2 = new Thread(() ->{
Scanner scanner = new Scanner(System.in);
System.out.println("请输入 isQuit 的值");
isQuit = scanner.nextInt();
});
t1.start();
t2.start();
}
}
しかし、プログラムを実行すると問題が見つかりました。0 以外の値を入力すると、isQuit の値は変更されましたが、t1 スレッドはまだ実行中です。
プログラムがコンパイルされて実行されると、Java コンパイラーと jvm はコードに対していくつかの「最適化」を実行する場合があります。
コンパイラの最適化は基本的に、作成したコードをインテリジェントに分析、判断、調整するコードに依存します。ほとんどの場合、この調整は問題なく、ロジックが変更されないことを保証できます。ただし、マルチスレッドが発生した場合、このエラーが発生する可能性があります。最適化により、プログラム内の元のロジックが変更されます。
この条件判断は基本的に次の 2 つの指示を行います。
1.ロード(メモリの読み取り)、メモリの読み取り動作速度が非常に遅い
2. jcmp (比較とジャンプ)、この比較はレジスタ操作であり、非常に高速です。
この時点で、コンパイラ/JVM は、このロジックでは、コードが同じメモリ値を繰り返し素早く読み取る必要があり、このメモリの値は読み出されるたびに同じであることを発見しました。そのため、コンパイラは太字のコードを作成します。決定 決定:ロード操作を直接最適化し、今後はロードせず、レジスタ内のデータを直接比較します。
しかし、プログラマが別スレッドで isQuit の値を変更することは想定しておらず、このときコンパイラは t2 スレッドを実行するかどうか、いつ実行するかを正確に判断できず、誤判定が発生しました。
ここでメモリ上の isQuit の値が変更されますが、isQuit の値が別のスレッドで繰り返し読み取られるわけではないため、 t1 スレッドは t2 の変更を感知できず、上記の問題が発生します。
volatile キーワードは、上記の欠点を補うために使用されます。
volatile を使用して変数を変更した後、コンパイラは、この変数が「揮発性」であり、上記の方法でレジスタへの読み取り操作を最適化できないことを理解します (コンパイラは上記の最適化を無効にします)。ループ中は常にメモリ内のデータを読み取ります。
ソース コードに volatile を追加すると、t1 スレッドが正常に終了できることがわかります。
Volatile は基本的に変数のメモリ可視性を保証します (変数の読み取り操作が読み取りレジスタに最適化されることを禁止します)。
コンパイラの最適化は実際には形而上学的な問題であり、いつ最適化すべきか、いつ最適化すべきでないかというルールを理解することはできません。
上記のコードを少し変更すると、上記の最適化がトリガーされない可能性があります。たとえば、次のようになります。
sleep を追加すると while ループの速度に大きく影響します. 速度が遅いとコンパイラによる最適化が続行されません. このとき volatile を追加しなくてもメモリの変化をすぐに感知できます.わかった
補足:ワーキングメモリ(ワークメモリ)とは、今回のノイマン型アーキテクチャにおけるメモリではなく、CPUレジスタ+CPUキャッシュのことを総称して「ワーキングメモリ」と呼びます。
メモリの可視性の問題:
1. コンパイラの最適化
2. メモリモデル
3. マルチスレッド
volatile はアトミック性ではなくメモリの可視性を保証します。!!
6.待って通知する
待機と通知もマルチスレッドにおける重要なツールです
マルチスレッドのスケジューリングは「ランダム」です。多くの場合、スレッド間の連携を完了するために、指定した順序で複数のスレッドが実行できることが望まれます。
wait と notification は、スレッドの順序を調整するための重要なツールです。
この2つのメソッドはObjectが提供するメソッドであり、どのオブジェクトに対してもwaitとnotifyを使用することができます。
ここで、wait メソッドを使用しようとすると、コンパイラは次のプロンプトを表示します。
意味:待機によりスレッドがブロックされる場合、割り込みメソッドを使用してスレッドをウェイクアップし、現在のスレッドのブロック状態を中断できます。
次に、例外をスローし、コードを再度実行すると、例外が発生したことがわかります。
意味: 不正なロック状態の例外
ロック状態はロックとロック解除の 2 つだけです。
では、なぜエラーが発生するのでしょうか?
wait が再度実行されると、次の 3 つのことが行われます。
1. ロックを解除します。object.wait はオブジェクト object のロックを解除しようとします。
2. ブロックして待機する
3. 他のスレッドによってウェイクアップされると、再ロックを試行し、ロックが成功すると待機が実行され、他のロジックが引き続き実行されます。
ロック解除を待機するための前提条件は、最初にロックすることですが、まだロックすらしていません。
中心的なアイデア: 最初にロックし、次に同期して待機する
このコードをロックしてプログラムを実行すると、正常にブロック状態に入ったことがわかり、ここでの待機により他のスレッドが通知するまでブロックされます。
待機と通知の主な目的は、スレッド間の実行順序を調整することです。最も典型的なシナリオの 1 つは、「スレッドの飢餓/飢餓」を効果的に回避することです。
待機して通知するデモ:
//wait 和 notify
public class Demo15 {
//使用这个锁对象来负责加锁,wait,notify
private static Object locker = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
while(true){
synchronized (locker){
System.out.println("t1 wait 开始");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 wait 结束");
}
}
});
t1.start();
Thread t2 = new Thread(()->{
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker){
System.out.println("t2 notify 开始");
locker.notify();
System.out.println("t2 notify 结束");
}
}
});
t2.start();
}
}
実行効果:
いくつかのメモ:
1. 待機をスムーズにウェイクアップするために通知する場合は、待機と通知が同じオブジェクトを使用して呼び出されることを確認する必要があります。
2. waitとnotifyは両方とも同期する必要があるnotifyは「ロック解除操作」を伴いませんが、Javaではnotifyを同期する必要があります。
3. 通知実行時に他のスレッドが待ち状態にない場合、通知は「無駄撃ち」に相当し、副作用はありません。
i 個のスレッドが存在する可能性があります。たとえば、待機用のスレッドが N 個存在する可能性があり、1 つのスレッドが通知を担当します。このとき、通知操作は 1 つのスレッドのみをウェイクアップします。どのスレッドがウェイクアップされるかはランダムです。
NoticeAll は待機中のすべてのスレッドをウェイクアップできます
指定したスレッドをウェイクアップしたい場合は、別のスレッドに別のオブジェクトを使用して待機させることができ、誰かをウェイクアップしたい場合は、対応するオブジェクトを使用して通知することができます
7. 待機とスリープの比較
スリープには明確な時間があり、その時間になると自然に起きます。事前に起きることもできます。割り込みを使用するだけです。
デフォルトでは、wait は他のスレッドからの通知を待つデッドウェイティング状態になっていますが、割り込みによって事前に wait を解除することもできます。
通知はスムーズなウェイクアップとして理解できます。ウェイクアップ後、スレッドは動作を継続する必要があり、後で待機状態に入ります。
割り込みは、スレッドがもうすぐ終了することをスレッドに伝え、スレッドは仕上げ作業に入ります。
wait にはタイムアウトのあるバージョンもあります (join と同様)
したがって、複数のスレッド間の実行順序を調整し、当然ながらスリープではなく待機と通知を使用することを優先します。
5. マルチスレッドの場合
1. シングルトンモード
シングルトンパターンはデザインパターンです
デザイン パターンはプログラマーのチェス ゲームであり、多くの典型的なシナリオと典型的なシナリオへの対処方法を紹介します。
シングルトン モードに対応するシナリオ: 場合によっては、一部のオブジェクトがプログラム全体でインスタンス (オブジェクト) を 1 つだけ持つこと、つまり新規にできるのは 1 回だけであることが望まれることがあります。
人に保証を頼るのは当てにならない。ここでは、コンパイラに頼ってより厳格なチェックを実行し、コードを 1 回だけ new オブジェクトにする方法を見つける必要があります。new が複数回試行されると、エラーが直接報告されます。
Java ではこの効果を実現する方法がたくさんありますが、ここでは主に 2 つの方法を紹介します。
1. ハングリー モード (緊急) プログラムが開始され、クラスがロードされた直後にインスタンスが作成されます。
2. 遅延モード (遅延) では、初めて使用するときにインスタンスが作成されます。それ以外の場合は、可能であればインスタンスは作成されません。
例えば:
コンパイラはファイルを開きます。非常に大きなファイルがあるとします。一部のコンパイラは、すべての内容を一度にメモリにロードします (ハングリーマン)。また、一部のコンパイラは、内容の一部のみをロードし、他の部分はページングされます。ユーザーです。時間が来たら、必要なときにもう一度ロードしてください(怠け者)
(1) ハングリーマンモード
staticの場合はクラスの属性を表しますが、各クラスのクラスオブジェクトがシングルトンなので、クラスオブジェクト(static)の属性もシングルトンになります。
コードに制限を加える場合: 他の人がこのインスタンスを新規作成することを禁止する、つまり構築メソッドをプライベートに変更する
このコードの実際の実行は、Singleton クラスが jvm によってロードされるときです。Singleton クラスは、プログラムの開始時ではなく、jvm が初めて使用されるときにロードされます。
現時点では、このコードはコンパイルされ、エラーが報告されます。
現時点では、使用できるインスタンスは1 つ。このように書くことで効率的にシングルトンモードを実装することができます。
ということは、コンストラクターをprivateに設定すると、絶対に外部から呼び出せなくなるのでしょうか?
リフレクションを使用すると、実際に現在のシングルトン モードで複数のインスタンスを作成できます。ただし、リフレクション自体は「型破りな」プログラミング手法です。通常の開発では、慎重に使用する必要があります。リフレクションを乱用すると、コードがより抽象化され、保守が困難になるため、大きなリスクが生じます。
//单例模式(饿汉模式)
class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
//做出一个限制:禁止别人去 new 这个实例
private Singleton(){
}
}
public class Demo16 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
//Singleton s3 = new Singleton(); 会报错
System.out.println(s1 == s2);
//System.out.println(s1 == s3);
}
}
(2) レイジーモード
class Singletonlazy{
private static Singletonlazy instance = null;
public static Singletonlazy getInstance(){
if (instance == null){
instance = new Singletonlazy();
}
return instance;
}
private Singletonlazy(){
}
}
//单例模式(懒汉模式)
public class Demo17 {
public static void main(String[] args) {
Singletonlazy s1 = Singletonlazy.getInstance();
Singletonlazy s2 = Singletonlazy.getInstance();
System.out.println(s1 == s2);
}
}
前部分はすべて伏線ですが、本題に入りましょう。
先ほど述べた 2 つのモードのうち、どちらがスレッド セーフです。つまり、複数のスレッドが getInstance を呼び出す場合、どのコードがスレッド セーフです (バグはありません)。
ハングリーマンモードの場合: マルチスレッドでは、instancece の内容を読み取り、マルチスレッドが同じ変数を読み取ることに問題はありません。
遅延モードの場合: ここで問題が発生します
この時点で、SingletonLazy は新規で 2 つのオブジェクトを作成し、シングルトンではなくなります。
このとき、次のような疑問が生じます。2 番目の新しい操作は、元のインスタンスの参照を変更しないのですか? 以前新規だったオブジェクトはすぐにリサイクルされませんでしたか?最終的にはオブジェクトが 1 つだけ残ったのではありませんか?
注: オブジェクトの新規作成のオーバーヘッドは非常に大きくなる可能性があり、複数回新規作成すると、多量のオーバーヘッドが発生します。
結論: 遅延モードのコードはスレッドセーフではありません。!!
(3) 遅延モードのスレッド安全性の問題を解決する
この問題はロックすることで解決できるため、次のコードになります。
ただし、この書き方は間違っており、 上記のスレッド セーフティの問題は解決されません。これは、操作自体がアトミックであり、再度ロックしても実質的な変更は行われないためです。
では、どのように変更すればよいのでしょうか?
外側にロックを追加します。
public static Singletonlazy getInstance(){
synchronized (Singletonlazy.class){
if (instance == null){
instance = new Singletonlazy();
}
}
return instance;
}
このようにして、上記の問題は解決されましたが、他にもまだ問題があります。
ロックは比較的高価な操作であり、ロックによりブロック待機が発生する可能性があります。
ロックの基本原則は、「必要な場合以外はロックしない」、「むやみにロックしない」です。何も考えずにロックを行ってしまうと、プログラムの実行効率に影響を及ぼします。
この書き方は、後続の GetInstance 呼び出しをすべてロックする必要があることを意味しますが、必ずしもロックする必要はありません。
遅延モードのスレッドは安全ではありません。この問題は主に、オブジェクトが初めて新しい場合に発生します。オブジェクトが新しい場合は、後で getInstance を呼び出しても問題はありません。
つまり、オブジェクトが新規になると、その後の if 条件は入力できなくなり、変更操作は行われず、読み取り操作のみが行われます。
この書き方では、最初の呼び出しとそれ以降の呼び出しの両方でロックが行われますが、実際にはそれ以降の呼び出しではロックする必要はありません。ここで、ロックすべきではない場所がロックされ、処理の効率に大きく影響します。プログラム。
では、そのような問題をどのように解決すればよいでしょうか?
まずロックするかどうかを決定し、次に実際にロックするかどうかを決定します。
public static Singletonlazy getInstance(){
// instance 如果为空,就说明是首次调用,需要加锁,如果是非null,就说明是后续调用,不用加锁
if (instance == null){
synchronized (Singletonlazy.class){
if (instance == null){
instance = new Singletonlazy();
}
}
}
return instance;
}
同じ if 条件を 2 回書くのは合理的ですか?
とても合理的です!!!
これは、ロック操作がブロックされる可能性があり、ブロックされる期間を判断することができないためです。2 番目の条件と最初の条件の間には非常に長い時間間隔があり、この長い時間間隔の間に他のスレッドがインスタンスを変更する可能性があります。
最初の条件: ロックするかどうかを決定する
2 番目の条件: オブジェクトを作成するかどうかを決定する
(4) 遅延モードでのメモリ可視性の問題
同時に実行している 2 つのスレッドがあるとします。インスタンスを変更した後、最初のスレッドはコード ブロックを終了し、ロックを解放します。2 番目のスレッドはロックを取得し、ブロックから回復できます。その後、2 番目のスレッドは読み取り操作を実行します。 2 つのスレッドによって実行された場合、最初のスレッドによって変更された値を読み取ることができますか?
これはメモリの可視性の問題です
ここで、インスタンスに volatile を追加する必要があります。
ここでメモリの可視性の問題が発生するかどうかについては、そのようなリスクがある可能性があることのみを分析しましたが、このシナリオでコンパイラが実際に最適化をトリガーするかどうかは不明です。
これは実際には前のコードと根本的に異なります: 以前のメモリ可視性は 1 つのスレッドによって繰り返し読み取られていましたが、現在は複数のスレッドによって繰り返し読み取られています。この時点で以前の最適化がトリガーされるかどうかは不明ですが、volatile を追加するとより効果的です。安全な練習
ここに volatile を追加することには別の目的があります。それは、ここでの代入操作の命令の並べ替えを避けるためです。
命令の並べ替えは、コンパイラの最適化の手段でもあります。元の実行ロジックは変更されないことを保証しながら、コードの実行順序が調整され、調整後の実行効率が向上します。
シングルスレッドの場合、このような並べ替えは通常問題ありませんが、マルチスレッドの場合は問題が発生する可能性があります。
この操作には次の 3 つの手順が含まれます。
1. オブジェクトのメモリ空間を作成し、メモリ アドレスを取得します。
2. スペース上でコンストラクター メソッドを呼び出してオブジェクトを初期化します。
3. メモリアドレスをインスタンス参照に割り当てます。
これには命令の並べ替えが含まれる可能性があり、123 が 132 になる可能性があります。 シングルスレッドの場合、この時点で 2 が最初に実行されるか 3 が最初に実行されるかは問題ではありませんが、マルチスレッドの場合は必ずしも当てはまりません。
または、2 つのスレッドが同時に実行されていると仮定します。
最初のスレッドが1 3 2 の順に実行され、3 の実行後、2 の前でスレッドの切り替えが発生したとすると、オブジェクトの初期化が間に合わないうちに他のスレッドにスケジュールされます。
次に、2 番目のスレッドが実行され、インスタンスが決定されます。= null であるため、インスタンスは直接返され、インスタンス内の一部の属性またはメソッドは後で使用される可能性がありますが、ここで取得されたオブジェクトは不完全で初期化されていないオブジェクトです。この不完全なオブジェクトには属性/メソッドを使用してください。場合によっては、状況が発生する可能性があります。
インスタンスに volatile を追加した後、インスタンスに対して実行される割り当て操作では、上記の命令の並べ替えは発生せず、132 ではなく 123 の順序で実行する必要があります。
class Singletonlazy{
private static volatile Singletonlazy instance = null;
public static Singletonlazy getInstance(){
// instance 如果为空,就说明是首次调用,需要加锁,如果是非null,就说明是后续调用,不用加锁
if (instance == null){
synchronized (Singletonlazy.class){
if (instance == null){
instance = new Singletonlazy();
}
}
}
return instance;
}
private Singletonlazy(){
}
}
2. キューのブロック
ブロッキングキューにはブロッキング機能があります。
1. キューがいっぱいの場合、キューに入り続けると、他のスレッドがキューから要素を取得するまでブロックが発生します。
2. キューが空のときにデキューを続けると、他のスレッドがキューに要素を追加するまでブロックが発生します。
ブロッキング キューは非常に便利です。バックエンド開発では、ブロッキング キューに基づいて、プロデューサー/コンシューマー モデルを実装できます。
プロデューサー/コンシューマー モデルは、マルチスレッドの問題に対処する方法です。次に例を示します。
餃子の作り方は3つのステップがあります。 1. 生地をこねる 2. 餃子の皮を伸ばす 3. 餃子を作る
旧正月に家族で餃子を作るときの戦略は 2 つあります。
1. 餃子の皮を伸ばす作業は各自が担当しており、伸ばした後に包むのは比較的効率が悪いです。
2. 1人が餃子の皮を広げる担当、残りの2人が餃子を作る担当【産消モデル】
生産者:餃子の皮を伸ばす人 消費者:餃子を作る人 取引場所:餃子の皮が置かれている場所
生産者/消費者モデルの利点:
1. デカップリング
デカップリングとは「モジュール間の結合を減らす」ことを意味します。
分散システムを考えてみましょう。
コンピュータ室に A と B だけがいる場合、A はリクエストを B に直接送信し、A と B の間の結合がより明白になります。
(1) B が死亡すると、B に大きな影響が及ぶ可能性があり、A が死亡すると、B にも大きな影響が及ぶ可能性があります。
(2) 今回サーバーCを追加すると、Aのコードを大幅に変更する必要があります。
このとき、生産者・消費者モデルを導入し、ブロッキングキューを導入すると、上記の問題を効果的に解決できる。
この時点で、A と B はブロッキング キューを通じて適切に分離されます。
このとき、A または B が電話を切っても、両者は直接やり取りしないため、大きな影響はありません。
新しいサーバー C を追加する場合、現時点ではサーバー A を変更する必要はありません。C にもキューから要素を取得させるだけです。
2. ピークの削りと谷の埋め込み
サーバーがクライアント/ユーザーから受信するリクエストは静的なものではなく、緊急事態によってリクエストの数が急激に増加する可能性があります。
サーバーが同時に処理できるリクエストの数には上限があり、サーバーごとに上限が異なります。
分散システムでは、より大きな圧力に耐えられるものと、より低い圧力に耐えられるものがあることがよくあります。
このとき、A がリクエストを受信するたびに、B は即座にリクエストを処理する必要があります。
A がより大きな圧力に耐えることができ、B がより低い圧力に耐えることができる場合、B が先に死ぬ可能性があります。
しかし、生産者消費モデルを使用する場合は別の話になります。
外部リクエストが突然急増し、A がさらに多くのリクエストを受信した場合、A はさらに多くのリクエスト データをキューに書き込みますが、B は元のリズムに従ってリクエストを処理できるため、ハングアップすることはありません。
キューがバッファとして機能するのと同じで、元々 B にかかっていた圧力をキューが引き受けます(ピークカット)
多くの場合、ピークは一時的なものです。ピークが沈静化すると、A が受け取るリクエストは減りますが、B は依然として確立されたリズムに従ってリクエストを処理し、B がアイドル状態になりすぎることはなくなります (谷を埋める) 。
プロデューサー/コンシューマー モデルは非常に重要であるため、ブロッキング キューは単なるデータ構造ですが、このデータ構造をサーバー プログラムに個別に実装し、別のホスト/ホスト クラスターを使用してデプロイします。ブロッキングキューが「メッセージキュー」に進化
Java 標準ライブラリは、ブロッキング キューの既製の実装をすでに提供しています。
配列 このバージョンは高速ですが、要素の最大数がわかっている場合に限ります。
要素がいくつあるかわからない場合は、Linked を使用する方が適切です(配列の場合、頻繁な展開は大きなオーバーヘッドになります)。
BlockingQueue の場合、オファーとポーリングにはブロック機能がありませんが、プットとテイクにはブロック機能があります。
ブロッキング キューに基づいた単純なプロデューサー/コンシューマー モデルを作成します。
1 つのスレッドが生成し、1 つのスレッドが消費します
//生产者消费者模型
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class Demo19 {
public static void main(String[] args) {
//搞一个阻塞队列,作为交易场所
BlockingQueue<Integer> queue = new LinkedBlockingDeque<>();
//负责生产元素
Thread t1 = new Thread(() ->{
int count = 0;
while(true){
try {
queue.put(count);
System.out.println("生产元素: " + count);
count++;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//负责消费元素
Thread t2 = new Thread(() ->{
while(true){
try {
Integer n = queue.take();
System.out.println("消费元素: " + n);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
}
ブロッキング キューを自分で実装します。
ここでは、ブロッキング キューは配列と循環キューに基づいて実装されています。
配列を端から端まで接続してリングを形成します。先頭と末尾が重なったとき、それらは空ですか、それともいっぱいですか?
(1) グリッドは無駄であり、テールがヘッドの前の位置に到達すると、グリッドはフルであると見なされます。
(2) 別の変数を作成し、別の変数を使用して現在の要素数を表します。
これで、単純なキューが実装されました。
class MyBlockingQueue{
//使用一个 String 类型的数组来保存元素,假设这里只存 String
private String[] items = new String[1000];
//指向队列的头部
private int head = 0;
//指向队列的尾部的下一个元素,总的来说,队列中的有效元素范围:[head,tail)
//当 head 和 tail 相等(重合),相当于空的队列
private int tail = 0;
//使用 size 来表示元素个数
private int size = 0;
//入队列
public void put(String elem){
if (size >= items.length){
//队列满了
return;
}
items[tail] = elem;
tail++;
if (tail >= items.length){
tail = 0;
}
size++;
}
//出队列
public String take(){
if (size == 0){
//队列为空,暂时不能出队列
return null;
}
String elem = items[head];
head++;
if (head >= items.length){
head = 0;
}
size--;
return elem;
}
}
次に、上記の通常のキューをブロッキング キューに変換します。
(1) スレッドセーフ
複数のスレッドから呼び出されたときにスレッドの安全性を確保するために、最初にロックを設定および取得します。
そこで、put と take に synchronized を直接追加します。
ロックに加えて、メモリの可視性の問題も考慮する必要があります
このとき、head、tail、size に volatile を追加します。
(2) ブロッキングの実装
(1) キューがいっぱいの場合、別の put によりブロックが発生します。
(2) キューが空の場合、再度取得するとブロッキングが発生します。
ここでの待機は 2 つ同時に出現することはありません。ここで待機するか、反対側で待機してください。
上記のコードでは条件が成立したらwaitを行っていますが、waitが起きると条件は破られるのでしょうか?
たとえば、現在、キューがいっぱいのため put 操作がブロックされています。しばらくすると、wait が目覚めます。目覚めると、この時点でキューはいっぱいになりますか? キューがまだいっぱいである可能性はありますか?
ウェイクアップの場合、キューはまだいっぱいです。これは、後続のコードが引き続き実行され、以前に保存された要素が上書きされる可能性があることを意味します。
現在のコードでは、割り込みによって呼び起こされると、この時点で直接例外が発生し、メソッドは終了して実行を継続しません。これにより、前述の既存の要素が上書きされるという問題は発生しません。
しかし、try-catchメソッドに従って書くと、この時点で割り込みが起きると、コードはダウンしてcatchに入り、catchが実行された後、コードは終了せずに実行を続けます。これにより、「要素のオーバーライド」ロジックがトリガーされます
上記の分析の結果、このコードはたまたま正しく書かれただけであることがわかりました。少しでも変更すると、問題が発生します。実際にこのように書いていると、この詳細に気づきにくいです。このコードを確実に実行する方法はありますか? try-catch で書かれていれば大丈夫でしょうか?
目覚めてから再度状態を判断するのを待ってください
キューがまだ満杯である場合は待機を続行し、キューが満杯でない場合は実行を続行できます。
ここでの while の目的はループすることではなく、ループを利用して上手に wait を実装することです。
したがって、waitを使用する場合は条件判定にwhileを使用することを推奨します。
最終的なコードは次のとおりです。
class MyBlockingQueue{
//使用一个 String 类型的数组来保存元素,假设这里只存 String
private String[] items = new String[1000];
//指向队列的头部
volatile private int head = 0;
//指向队列的尾部的下一个元素,总的来说,队列中的有效元素范围:[head,tail)
//当 head 和 tail 相等(重合),相当于空的队列
volatile private int tail = 0;
//使用 size 来表示元素个数
volatile private int size = 0;
//入队列
public synchronized void put(String elem) throws InterruptedException {
while (size >= items.length){
//队列满了,
this.wait();
}
items[tail] = elem;
tail++;
if (tail >= items.length){
tail = 0;
}
size++;
//用来唤醒队列为 空 的阻塞情况
this.notify();
}
//出队列
public synchronized String take() throws InterruptedException {
while (size == 0){
//队列为空,暂时不能出队列
this.wait();
}
String elem = items[head];
head++;
if (head >= items.length){
head = 0;
}
size--;
//使用这个 notify 来唤醒队列满的阻塞情况
this.notify();
return elem;
}
}
3. タイマー
タイマーも目覚まし時計と同様、日常の開発で一般的なコンポーネントです。
TimerTask は前に学習した Runnable に似ており、現在のタスクがいつ実行されるかを記録します。
タイマーに登録されたタスクは、スケジュールを呼び出したスレッドでは実行されず、タイマー内のスレッドで実行されます。
import java.util.Timer;
import java.util.TimerTask;
public class demo21 {
public static void main(String[] args) {
Timer timer = new Timer();
//给 timer 中注册的这个任务,不是在调用 schedule 的线程中执行的,而是通过 Timer 内部的线程来负责执行的
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时任务执行");
}
},3000);
System.out.println("程序开始运行");
}
}
結果を実行した後、コードは出力後にプロセスを終了していないことがわかります。
それの訳は:
タイマーには内部的に独自のスレッドがあり、新しくスケジュールされたタスクをいつでも処理できるようにするために、このスレッドは実行を継続し、このスレッドはフォアグラウンド スレッドでもあります。
次に、タイマーを自分で実装してみます。
タイマーには多くのタスクを設定できます。
まず、タスクを記述でき、次にデータ構造を使用して複数のタスクを整理できなければなりません。
(1) タスクを表す TimerTask のようなクラスを作成します このタスクには、タスクの内容とタスクの実際の実行時間の 2 つの側面が含まれる必要があります。
タスクの実際の実行時間はタイムスタンプで表すことができ、スケジューリングの際には、まず現在のシステム時刻を取得し、これに基づいて遅延時間を加算して、実際のタスクの実行時間を取得します。
(2) 特定のデータ構造を使用して複数の TimerTask を編成する
リスト (配列、リンク リスト) を使用してタイマータスクを整理する場合、タスクが多すぎる場合、どのタスクをいつ実行できるかをどのように決定すればよいでしょうか?
このように、上記の List を継続的に走査して、ここの各要素が時間に達したかどうかを確認するスレッドを作成する必要があります。時間に達した場合は実行され、時間に達していない場合はスキップされます。
しかし、この考えは科学的ではありません。これらのタスクの時間がまだ早すぎる場合、ここでのスキャン スレッドは時間になる前に繰り返しスキャンする必要があります。このプロセスは非常に非効率です。
(a) すべてのタスクをスキャンする必要はありません。最も早い時間のタスクに焦点を当てるだけです。
最も早いタスク時刻がまだ到着していない場合、他のタスク時刻はさらに到着しません。
すべてのタスクを走査して 1 つのタスクだけに集中できるように改善しました。
では、どのタスクが最優先であるかをどうやって知ることができるのでしょうか?
すべてのタスクを整理するには優先キューを使用することが最も適切であり、チームの最初の要素は時間の最も短いタスクです。
(b) このタスクのスキャンは繰り返し実行する必要はありません。
代わりに、キューの最初の要素の時刻を取得した後、現在のシステム時刻との差が作成され、この差に基づいてスリープ/待機時間が決定されます。この時刻に達するまでは、繰り返しスキャンは実行されません。スキャン回数が大幅に削減されます
ここでの「効率の向上」とは、実行時間を短縮することを意味するのではなく、リソース使用率を削減し、不必要な CPU の無駄を避けることを意味します。
(c) チームの先頭要素のタスクの有効期限が切れたかどうかを監視するスキャン スレッドを作成します。
このコードの中心的なプロセスは次のとおりです。
import java.util.PriorityQueue;
//创建一个类,用来描述定时器中的一个任务
class MyTimerTask{
//任务啥时候执行,毫秒级的时间戳
private long time;
//任务具体是啥
private Runnable runnable;
public long getTime() {
return time;
}
public Runnable getRunnable() {
return runnable;
}
public MyTimerTask(Runnable ruunnabl, long delay){
//delay 是一个相对的时间差
//构造 time 要根据当前系统时间和 deley 进行构造
time = System.currentTimeMillis() + delay;
this.runnable = ruunnabl;
}
}
//定时器类的本体
class MyTimer{
//使用优先级队列,来保存上述的 N 个任务
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
//定时器的核心方法,就是把要执行的任务给添加到队列中
public void schedule(Runnable runnable,long delay){
MyTimerTask task = new MyTimerTask(runnable,delay);
queue.offer(task);
}
//MyTimer 中,还需要构造一个 “扫描线程” ,一方面去负责监控队首元素是否到点了们是否应该执行;一方面当任务到店之后
// 就要调用这里的 Runnable 的 Run 方法来完成任务
public MyTimer(){
//扫描线程
Thread t = new Thread(() ->{
while(true){
if (queue.isEmpty()){
//注意,当前队列为空,此时就不应该去取这里的元素
continue;
}
MyTimerTask task = queue.peek();
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()){
//需要执行任务
queue.poll();
task.getRunnable().run();
}else {
//让当前扫描线程休眠一下,就可以按照时间差进行休眠
try {
Thread.sleep(task.getTime() - curTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t.start();
}
}
上記のコードでは、タイマーのコア ロジックが記述されていますが、このコードにはまだいくつかの重要な問題があります。
1. スレッドセーフではない
このコレクション クラスはスレッドセーフではなく、メイン スレッドとスキャン スレッドの両方で使用されます。
キュー上のロック操作
2. スキャン スレッドでは、sleep を直接使用してスリープするのが適切ですか?
不適切
(a) スリープがブロッキングに入った後、ロックは解放されず、他のスレッドやここで実行されるスケジュールに影響を与えます。
(b) スリープ中は、早めに割り込むのは不便です (割り込みを使用して割り込むこともできますが、割り込みはスレッドを終了する必要があることを意味します)。
現在の先頭のタスクが 14:00 に実行され、現在時刻が 13:00 (1 時間睡眠) であるとします。この時点で新しいタスクを追加すると、新しいタスクは 13:30 に実行されます。今回は、新しいタスクが 13:30 に実行され、そのタスクが最も早く実行されるタスクになります。
新しいタスクが来るたびに、以前の休眠状態を目覚めさせ、最新のタスクのステータスに基づいて新たな判断を下せるようにしたいと考えています。
現時点では、スキャン スレッドがブロックされ、30 分間待機する可能性があります。
対照的に、wait の方が適切です。wait はタイムアウトを指定することもでき、事前に wait を起動することもできます。
3. クラスを作成した場合、そのオブジェクトを優先キューに入れることができますか?
優先キューに配置される要素を「比較可能」にする必要があります
Comparable または Comparator を使用してタスク間の比較ルールを定義する
ここ、MyTimerTask に Comparable が実装されています
最終的なコードは次のとおりです。
import java.util.PriorityQueue;
//创建一个类,用来描述定时器中的一个任务
class MyTimerTask implements Comparable<MyTimerTask>{
//任务啥时候执行,毫秒级的时间戳
private long time;
@Override
public int compareTo(MyTimerTask o) {
//把时间小的,优先级高,最终时间最小的元素,就会放到队首
return (int) (this.time - o.time);
}
//任务具体是啥
private Runnable runnable;
public long getTime() {
return time;
}
public Runnable getRunnable() {
return runnable;
}
public MyTimerTask(Runnable ruunnabl, long delay){
//delay 是一个相对的时间差
//构造 time 要根据当前系统时间和 deley 进行构造
time = System.currentTimeMillis() + delay;
this.runnable = ruunnabl;
}
}
//定时器类的本体
class MyTimer{
//用来加锁的对象
private Object locker = new Object();
//使用优先级队列,来保存上述的 N 个任务
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
//定时器的核心方法,就是把要执行的任务给添加到队列中
public void schedule(Runnable runnable,long delay){
synchronized (locker)
{
MyTimerTask task = new MyTimerTask(runnable,delay);
queue.offer(task);
//每次来新的任务,都唤醒一下之前的扫描线程,好让扫描线程根据最新的额任务情况重新规划等待时间
locker.notify();
}
}
//MyTimer 中,还需要构造一个 “扫描线程” ,一方面去负责监控队首元素是否到点了们是否应该执行;一方面当任务到店之后
// 就要调用这里的 Runnable 的 Run 方法来完成任务
public MyTimer(){
//扫描线程
Thread t = new Thread(() ->{
while(true){
synchronized (locker){
while (queue.isEmpty()){
//注意,当前队列为空,此时就不应该去取这里的元素
//此处使用 wait 等待更合适,如果使用 continue ,就会使这个线程的 while 循环运行的飞快
//也会陷入一个高频占用 cpu 的状态(忙等)
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
MyTimerTask task = queue.peek();
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()){
//需要执行任务
queue.poll();
task.getRunnable().run();
}else {
try {
//让当前扫描线程休眠一下,就可以按照时间差进行休眠
locker.wait(task.getTime() - curTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
t.start();
}
}
4. スレッドプール
プールは非常に重要なメソッドを作成します
スレッドの作成と破棄を頻繁に行う必要がある場合、この時点でのスレッドの作成と破棄にかかるコストは無視できません。
したがって、スレッド プールを使用して、事前にスレッドのウェーブを作成できます。後でスレッドを使用する必要がある場合は、プールから 1 つだけ取り出し、スレッドが使用されなくなったら、プールに戻します。
当初はスレッドを作成/破棄する必要がありましたが、現在は既成のスレッドをプールから取得してプールに戻すようになりました。
システムからスレッドを作成するよりも、プールから何かを取得する方が高速かつ効率的であるのはなぜですか?
システムからスレッドを作成する場合は、システム API を呼び出す必要があります。その後、オペレーティング システム カーネルがスレッド作成プロセスを完了します。
カーネルはすべてのプロセスにサービスを提供するため、これは制御できません。
スレッドプールからスレッドを取得する場合、カーネル内での上記の操作は事前に行われており、スレッドを取得する現在のプロセスは純粋なユーザーコード (純粋なユーザーモード) と制御可能によって完了します。
Java 標準ライブラリは既製のスレッド プールも提供します
ファクトリ パターン: オブジェクトの生成
一般に、オブジェクトは new およびコンストラクター メソッドを通じて作成されます。
ただし、コンストラクター メソッドには大きな欠陥があります。コンストラクター メソッドの名前はクラス名に固定されます。
一部のクラスは複数の異なる構築メソッドを必要としますが、構築メソッドの名前は固定されており、メソッドのオーバーロードによってのみ実装できます(パラメータの数と型は異なる必要があります)。
例: ここで、2 つの方法で構築したいとします。1 つはデカルト積座標に従って構築すること、もう 1 つは極座標に従って構築することです。これら 2 つの構築方法のパラメータの数とタイプは同じであり、それらは同じです。オーバーロードを構成することはできません。コンパイル中にエラーが報告されます。
この時点で、ファクトリー モードを使用して上記の問題を解決できます。
上記の問題を解決するには、ファクトリ パターンを使用します。コンストラクターを使用する代わりに、通常のメソッドを使用してオブジェクトを構築します。このようなメソッドの名前は任意で構いません。通常のメソッド内で新しいオブジェクトを作成します。
通常のメソッドの目的はオブジェクトを作成することであるため、そのようなメソッドは通常は静的です。
スレッド プール オブジェクトの準備ができたら、submit メソッドを使用してスレッド プールにタスクを追加できます。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//线程池
public class Demo23 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4);
for (int i = 0;i < 100;i++){
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
}
上記のスレッド プールに加えて、標準ライブラリでは、より豊富なインターフェイスを備えたスレッド プールも提供します: ThreadPoolExecutor
以前のスレッド プールは使いやすくするためにカプセル化されていますが、ThreadPoolExecutor には調整できるオプションが多数あり、ニーズをより適切に満たすことができます。
Java標準ライブラリのスレッドプール構築メソッドのパラメータと意味について話しましょう
スレッド プールを会社にたとえると、コア スレッドの数は正式な従業員の数に相当します。
スレッドの最大数は正社員+インターンの数です
会社の業務が忙しくないときはインターンは必要ありませんが、会社の業務が忙しいときは、タスクを分担してくれるインターンを見つけることができます。
これは、ビジー時にタスクを効率的に処理するためだけでなく、アイドル時にリソースが無駄にならないようにするためにも実行できます。
インターン スレッドは、アイドル状態が特定のしきい値を超えると破棄される可能性があります。
スレッド プール内には多くのタスクがあり、これらのタスクはブロッキング キューを使用して管理できます。
スレッド プールには組み込みのブロッキング キューを含めることができ、手動でブロッキング キューを指定できます。
ファクトリ パターン。このファクトリ クラスを通じてスレッドを作成します。
これがスレッド プール検査の焦点です: 拒否方法/拒否戦略
スレッド プールにはブロッキング キューがあります。ブロッキング キューがいっぱいになるとタスクが追加され続けます。これに対処するにはどうすればよいですか?
最初の拒否戦略:
例外を直接スローすると、スレッド プールは動作を停止します。
2 番目の拒否戦略:
この新しいタスクを追加したスレッドがこのタスクを実行します
3 番目の拒否戦略:
最も古いタスクを破棄し、新しいタスクを実行します
4 番目の拒否戦略:
新しいタスクを直接破棄し、前のタスクを続行します。
上記のスレッド プールには 2 つのグループがあります: 1 つのスレッド プールのグループはカプセル化された Executor であり、もう 1 つのスレッド プールのグループはネイティブ ThreadPoolExecutor です。主に実際のニーズに応じて、どちらかを使用できます。
次に、スレッド プールを自分でシミュレートして実装してみます。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
class MyThreadPool{
private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();
//通过这个方法,来把任务添加到线程池中
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
// n 表示线程池里面有几个线程
//创建了一个有固定数量的线程池
public MyThreadPool(int n){
for(int i = 0;i < n;i++){
Thread t = new Thread(() ->{
while(true){
//循环的取出任务,并执行
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
}
//线程池
public class Demo24 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool = new MyThreadPool(4);
for(int i = 0;i < 1000;i++){
pool.submit(new Runnable() {
@Override
public void run() {
//要执行的工作
System.out.println(Thread.currentThread().getName() + " hello");
}
});
}
}
}
このときのスケジューリングはランダムであるため、現在このスレッド プールに挿入されているタスクが実行されると、N 個のスレッドの作業負荷は完全に均等になるわけではありませんが、統計的にはタスクの負荷は均等になります。
さらに、もう 1 つの重要な質問があります。
スレッド プールを作成するとき、スレッドの数はどのようにして決められますか?
プロジェクトが異なれば、スレッドによって実行される作業も異なります。
一部のスレッドの作業は「CPU 集中型」であり、スレッドの作業はすべて計算です
ほとんどの作業は CPU で実行する必要があります。CPU は、作業を進める前に、作業を完了するためにコアを配置する必要があります。CPU に N 個のコアがあり、スレッドの数も N である場合、理想的な状況は次のようになります。各コアに 1 つあります。
スレッドが多数ある場合、スレッドはキュー内で待機しているため、新たに進行することはありません。
一部のスレッドの作業は、夜間の読み取りと書き込み、ユーザー入力の待機、ネットワーク通信など、「IO 集中型」です。
待ち時間が多くなりますが、待ち時間中はCPUを使用しないため、スレッド数が増えてもCPUに大きな負担をかけることはありません。
実際の開発では、スレッドの作業の一部は CPU 集中型であり、その作業の一部は IO 集中型であることがよくあります。
現時点では、CPU 上で実行されているスレッドの数と IO を待機しているスレッドの数は不明です。
ここでのより良いアプローチは、実験を通じて適切なスレッド数を見つけ、パフォーマンス テストを通じてさまざまなスレッド数を試し、試用プロセス中にパフォーマンスとシステム リソース オーバーヘッドの間でよりバランスのとれた値を見つけることです。
6. 一般的なロック戦略
1. 楽観的ロック VS 悲観的ロック
楽観的ロック: このシナリオではロックの競合が発生する可能性は低いと予測します。
悲観的なロック: このシナリオを予測すると、ロックの競合が非常に簡単に発生します。
ロックの競合: 2 つのスレッドがロックを取得しようとしますが、1 つのスレッドはロックを正常に取得できますが、もう 1 つのスレッドはブロックして待機します。
ロック競合の確率が高いか低いかは、その後の作業に一定の影響を与えます。
2. 重量ロック VS 軽量ロック
ヘビーウェイト ロック: ロックのコストは比較的大きくなります (時間がかかり、システム リソースの消費が少なくなります)。
軽量ロック: ロックにかかるコストは比較的小さい (所要時間が短くなり、システム リソースの消費も少なくなります)。
悲観的なロックは重量ロックである可能性があります (絶対的なものではありません)
楽観的ロック、おそらく軽量ロック (絶対的ではない)
悲観主義と楽観主義は、作業量を決定するロック前のロック競合の確率の予測に基づいており、軽量化は、ロック後の実際のロックのオーバーヘッドの考慮に基づいています。
公式には、このような概念が重なるため、特定のロックについては、楽観的ロックと呼ばれたり、軽量ロックと呼ばれたりすることがあります。
3. スピンロック VS サスペンド待機ロック
スピン ロック: 軽量ロックの典型的な実装です。
これは、多くの場合、ユーザー モードで、自己選択 (while ループ) を通じてロックのような効果が達成されることを意味します。
一定量の CPU リソースを消費しますが、できるだけ早くロックを取得できます。
一時停止待機ロック: 重量ロックの典型的な実装です。
カーネルの状態とシステムが提供するロック メカニズムを通じて、ロックの競合が発生すると、カーネルのスレッドのスケジューリングが関与し、競合するスレッドがハング (ブロックして待機) します。
4. 読み取り/書き込みロック VS ミューテックス ロック
読み取り/書き込みロック: 読み取り操作のロックと書き込み操作のロックを分離します。
複数のスレッドが同じ変数を同時に読み取る場合、スレッドの安全性の問題は発生しません。
2 つのスレッドがあり、1 つのスレッドが読み取りとロックを実行し、もう 1 つのスレッドも読み取りとロックを実行する場合、ロックの競合は発生しません。
2 つのスレッドがあり、一方のスレッドが書き込みロックされ、もう一方のスレッドも書き込みロックされている場合、ロックの競合が発生します。
スレッドが 2 つあり、一方のスレッドが書き込み用にロックされ、もう一方のスレッドが読み取り用にもロックされている場合、ロックの競合が発生します。
実際の開発では、読み取り操作の頻度が書き込み操作の頻度よりもはるかに高いことがよくあります。
Java 標準ライブラリは、既製の読み取り/書き込みロックも提供します
5. 公平なロック VS 不公平なロック
ここで定義されているように、公平なロックは先着順のロックです。
不公平なロックは確率が等しいように見えますが、実際には不公平です (各スレッドのブロック時間は異なります)。
オペレーティング システム自体のロック (pthread_mutex) は不公平なロックです。
公平なロックを実装するには、それをサポートする追加のデータ構造が必要です (たとえば、各スレッドのブロック待機時間を記録する方法が必要です)。
6. リエントラントロック VS 非リエントラントロック
スレッドが連続して 2 回ロックをロックすると、再入不可ロックであるデッドロックが発生します。
このようにコードを書いた場合、何か問題はあるのでしょうか?
1. メソッドを呼び出して、まずこれをロックします (この時点でロックは成功したものとします)。
2. 次に、コードブロック内で同期するために down を実行しますが、この時点ではまだロックされています。
このとき、このオブジェクトは既にロック状態にあるため、ロック競合が発生します。
現時点では、スレッドはロックを取得する機会が得られる前に、ロックが解放されるまでブロックされます。
このコードでは、このロックは増加メソッドの実行後にのみ解放できますが、メソッドの実行を続行するには、2 回目にロックが正常に取得される必要があります。
コードを下方向に実行し続けたい場合は、2 番目のロックを取得する、つまり最初のロックを解放する必要があります。最初のロックを解放したい場合は、コードが実行を継続することを確認する必要があります。
このとき、thisのロックが解除できないため、コードはここでスタックし、スレッドがフリーズします(デッドロックの最初の兆候)
非リエントラント ロックの場合、ロックはどのスレッドがロックしたかを保存しません。「ロック」リクエストを受信する限り、現在のスレッドがどのスレッドであるかに関係なく、現在のロックを拒否します。デッドロックが発生すると、起こる
リエントラント ロックは、どのスレッドがロックを追加したかを保存します。後続のロック要求を受信した後、まず、ロックされたスレッドが現在ロックを保持しているスレッドかどうかを比較します。この時点で、柔軟に決定できます。
synchronized 自体はリエントラント ロックであるため、このコードはデッドロックしません。
リエントラント ロックを使用すると、ロックは現在どのスレッドがロックを保持しているかを記録できます。
ロックが N レベルである場合、} に遭遇したとき、JVM は現在の } が最も外側のものであるかどうかをどのようにして知るのでしょうか?
ロックにカウンターを保持させるだけで、ロック オブジェクトに、どのスレッドがロックを保持しているかを記録するだけでなく、現在のスレッドが整数変数を介してロックを追加した回数も記録します。
ロック操作が発生するたびにカウンタは +1 され、ロック解除操作が発生するたびにカウンタは -1 になります。
カウンタが0になった時点で実際にロック解除動作が行われ、それ以外の場合はロック解除は行われません。
このカウントを「参照カウント」と呼びます。
7. デッドロック
1. 1 つのスレッド、1 つのロックですが、反復不可能なロックであるため、スレッドが連続して 2 回ロックをロックすると、デッドロックが発生します。
2. 2 つのスレッド、2 つのロック 2 つのスレッドは、まずそれぞれロックを取得し、次に相手のロックを同時に取得しようとします。
3. N スレッド、N ロック
コードを通して行き詰まりを感じてください。
public class Demo25 {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
synchronized (locker1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2){
System.out.println("t1 两把锁加锁成功");
}
}
});
Thread t2 = new Thread(() ->{
synchronized (locker2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker1){
System.out.println("t2 两把锁加锁成功");
}
}
});
t1.start();
t2.start();
}
}
このコードは典型的な 2 番目のタイプのデッドロックで、実行後、どちらのスレッドも結果を正常に出力できないことがわかります。
jconsole を開いてスレッドをさらに観察します。
両方のスレッドがさらにロックされた場所でスタックしていることがわかります。
サーバープログラムでデッドロックが発生すると、デッドロックが発生したスレッドがフリーズして動作を継続できなくなり、プログラムに重大な影響を及ぼします。
3. N スレッド、N ロック
ダイニング哲学者の問題:
テーブルの上に 5 人の哲学者と 5 本の箸があるとします。
すべての哲学者は主に次の 2 つのことを行う必要があります。
1.人生を考え、箸を置く
2.麺を食べるときは、左右の手で箸を持ち、麺をつまんで食べます。
その他の設定:
1. すべての哲学者がいつ人生について考えるのか、いつ麺類を食べるのかは不明です。
2. 哲学者は皆、麺を食べたいと思うと、麺を食べるという作業を完了するために非常に頑固であり、他人が自分の箸を使用すると、ブロックされて待ち、待っている間、彼は置きません。彼の手は箸を持っている
上記のモデル設定に基づいて、これらの哲学者はすべて非常にうまく機能します。
ただし、極端な状況が発生するとデッドロックが発生します
同じ瞬間に、5 人の哲学者が全員麺類を食べたいと思って、同時に左手を伸ばして左側の箸を持ち、次に右側の箸を取ろうとしたとします。
5 人の哲学者は 5 つの糸、5 つの箸は 5 つのロック
哲学者モデルは、3 番目のタイプのデッドロック問題を生き生きと説明します。
では、デッドロックを回避する方法はあるのでしょうか?
まずはデッドロックの原因とデッドロックの必要条件を明らかにしましょう。
必要条件は4つ(1つは必須)
いずれかの条件に違反できる限り、デッドロックは回避できます。
1. 相互排他を使用し、1 つのスレッドがロックを取得すると、他のスレッドはロックを取得できなくなります。
実際に使用されるロックは一般に相互に排他的です (ロックの基本特性)
2. プリエンプトすることはできません。ロックは保持者によってのみ能動的に解放でき、他のスレッドによって奪い取ることはできません。
それはロックの基本的な特性でもあります
3. リクエストとホールド: スレッドは複数のロックの取得を試行し、2 番目のロックの取得を試行している間、最初のロックの取得ステータスを維持します。
コード構造に依存します (要件に影響を与える可能性が最も高い)
4. ループ内で待機中、t1 はロッカー 2 を取得しようとします。t2 が実行を完了する必要があります。ロッカー 2 を解放します。t2 はロッカー 1 を取得しようとします。t1 が実行を完了する必要があります。ロッカー 1 を解放します。
コード構造に依存(デッドロック問題を解決するためのキーポイント)
デッドロック問題を具体的に解決するにはどうすればよいでしょうか? 実用的な手法は数多くあります(バンカーズアルゴリズム)
バンカーのアルゴリズムはデッドロック問題を解決できますが、あまり実用的ではありません。
ここでは、デッドロック問題を解決するためのより簡単で効果的な方法を紹介します。
ロックに番号を付け、ロックする順序を指定します。
たとえば、各スレッドが複数のロックを取得したい場合、最初に小さい番号のロックを取得し、次に大きい番号のロックを取得する必要があると規定されています。
すべてのスレッドのロック順序が上記の順序に厳密に従っている限り、ロックを待つことはありません。
8. 同期原理
1. 基本機能
同期では具体的にどのような戦略が使用されますか?
1. 同期は悲観的ロックと楽観的ロックの両方です。
2. 同期 重いロックでも軽量なロック
3. 同期された重量ロック部分はシステムのミューテックス ロックに基づいて実装され、軽量ロック部分はスピン ロックに基づいて実装されます。
4. 同期は不公平なロックです (先着順の原則に従いません。ロックが解放された後、どのスレッドがロックを取得するかはその能力に依存します)。
5. 同期は再入可能なロックです (どのスレッドがロックを取得したかを内部的に記録し、参照カウントを記録します)
6. 同期は読み取り/書き込みロックではありません
2.ロック工程
synchronized の内部実装戦略 (内部原則)
コードに同期が記述された後、ここで一連の「適応プロセス」とロックのアップグレード (ロック拡張) が発生する可能性があります。
ロックなし→バイアスロック→軽量ロック→重量ロック
バイアスされたロックは実際のロックではなく、単なる「マーク」です。他のスレッドがロックをめぐって競合すると、ロックは実際にロックされます。競合する他のスレッドがない場合、ロックは最初から最後まで実際にはロックされません。ロックされた
ロック自体には一定のコストがかかります。追加できない場合は追加しないでください。実際にロックを追加するには、誰かが競争する必要があります。
軽量ロック、スピン ロックを介して軽量ロックを同期実装
ここでロックが占有されている場合、別のスレッドはスピン メソッドに従って現在のロック ステータスが解放されているかどうかを繰り返し問い合わせます。
ただし、将来的にこのロックをめぐって競合するスレッドが増えると (ロックの競合が激しくなる場合)、同期は軽量ロックから重量ロックにアップグレードされます。
ロックの削除では、コンパイラは現在のコードをロックする必要があるかどうかをインテリジェントに判断します。最初にロックを記述しても、実際にはロックする必要がない場合、ロック操作は自動的に削除されます。
コンパイラは最適化を実行して、最適化後のロジックが以前のロジックと一貫していることを確認します。
これにより、一部の最適化が保守的になります
私たちプログラマーは、コードの効率を向上させるために最適化だけに頼っているわけではなく、私たち自身も何らかの役割を果たさなければなりません。いつロックするかの判断も非常に重要な作業です
ロックの粗面化
「ロック粒度」: ロック操作に実際に実行されるコードが多く含まれる場合、ロック粒度はより大きいとみなされます。
場合によっては、ロックの粒度がより小さく、同時性の度合いがより高いことが望まれることがあります。
ロックとロック解除自体にもオーバーヘッドがあるため、ロックの粒度を大きくした方が良い場合があります。
9. CAS
1.コンセプト
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
これは疑似コードの一部です
CAS に基づいて、一連の「ロックフリー」プログラミングを導き出すことができます。
CAS の使用範囲には一定の制限があります。
2. CASの適用
(1) アトミッククラスの実装
たとえば、複数のスレッドが count 変数に対して ++ を実行します。
Java標準ライブラリでは、アトミッククラスのセットが提供されています。
アトミック クラス内ではロック操作は実行されず、CAS を介してスレッドセーフな自動インクリメントのみが完了します。
import java.util.concurrent.atomic.AtomicInteger;
//使用原子类
public class Demo26 {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
for(int i = 0;i < 50000;i++){
//相当于 count++
count.getAndIncrement();
//相当于 ++count
//count.incrementAndGet();
//count--
//count.getAndDecrement();
//--count
//count.decrementAndGet();
}
});
Thread t2 = new Thread(() ->{
for (int i = 0;i < 50000;i++){
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count.get());
}
}
上記のアトミック クラスは CAS に基づいて実装されています
疑似コードの実装:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
2 つのスレッドが制限なしに ++ を同時に実行する場合、それは 2 つの ++ がシリアルであり、正しく計算できることを意味します。2 つの ++ 操作が散在する場合があります。この時点で問題が発生する可能性があります。
ロックによりスレッド メモリのセキュリティが確保され、ロックによりインターリーブが強制的に回避されます。
アトミック クラス/CAS はスレッドの安全性を保証します: CAS を使用して、現在「インターリーブ」があるかどうかを識別します。インターリーブがない場合は、この時点で直接変更します。これは安全です。インターリーブが発生した場合は、メモリ内の最新の値を再読み取りします。もう一度繰り返します。変更してみてください。
CAS は命令であり、命令自体を分割することはできません。
CAS 自体は 1 つの命令であり、実際にはメモリにアクセスする操作が含まれており、複数の CPU がメモリにアクセスしようとする場合、基本的にシーケンスが存在します。
これら 2 つの CAS が連続してメモリにアクセスするのはなぜですか?
同じリソースを操作する複数の CPU には、ロック競合 (命令レベルのロック) も含まれます。このロックは、通常呼ばれる同期されたコードレベルのロックよりもはるかに軽いです。
(2) スピンロックの実装
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
3. CAS の ABA 問題
CAS のポイントは、レジスタ 1 の値とメモリの値を比較し、それらが等しいかどうかによってメモリの値が変化したかどうかを判断することです。
メモリ値が変更された場合は、他のスレッドによって変更されています。
メモリ値が変更されておらず、他のスレッドがそれを変更していない場合、その後の変更は安全です。
しかし、ここで問題があります。ここの値が変更されていない場合、他のスレッドがその値を変更していないことを意味するのでしょうか?
A - B - A 問題: 別のスレッドが変数を A -> B に変更し、次に B -> A に変更します。この時点で、現在のスレッドは値が一度も変更されていないのか、または変更されてから変更されたのかを区別できません。戻る。
ほとんどの場合、ABA に問題があっても影響はありませんが、極端なシナリオに遭遇した場合は、そうでない可能性があります。
現在口座に 100 元があり、50 元を引き出したいとします。
極端な状況が発生したとします。お金を引き出すために最初のボタンを押すと、ボタンが動かなくなるので、もう一度押すとします (2 つの引き出しリクエストが生成され、ATM はこれら 2 つのリクエストを処理するために 2 つのスレッドを使用します)。
出金が CAS 方式に従って行われると仮定すると、各スレッドは次のように動作します。
1. 口座残高を読み取り、変数 M に代入します。
2. CAS を使用して、現在の実際の金額がまだ M であるかどうかを判断します。そうである場合は、実際の残高を M - 50 に変更します。そうでない場合は、現在の操作を中止します (操作は失敗します)。
2 つのスレッド t1 と t2 のみが 50 の引き出し操作を実行する場合、CAS により、お金は 1 回しか正常に引き落とされません。
しかし、この時点で別の人が 50 元を口座に送金したとします。そのため、t2 操作が完了した後、残高は最初の 100 元に戻り、t1 は他のスレッドが残高を変更していないと誤って認識し、継続 演繹操作を行った結果、演繹が繰り返され、このとき A-B-A 問題が発生しました。
上記の操作が行われる可能性は比較的低いですが、それでも考慮する必要があります。
ABA 問題に関しては、CAS の基本的な考え方は問題ありませんが、主な理由は、変更操作が繰り返しジャンプする可能性があり、CAS の判断が簡単に無効になる可能性があることです。
CASは「値が同じである」と判断しますが、実際には「値が変化していない」ことを期待しています
値が一方向にのみ変更できることが合意されている場合 (たとえば、一方向にのみ増加でき、減少できない)、問題は簡単に解決されます。
しかし、口座残高は増えるだけで減ることはできないようです。
バランスは機能しませんが、バージョン番号は機能します。!!
このとき、残高が変更されたかどうかを測定するには、残高の値だけではなく、バージョン番号を見ます。
アカウント残高の隣を配置します: バージョン番号 (値は増加します、減少しません)
CAS を使用して、バージョン番号が同じかどうかを確認します。バージョン番号が同じ場合は、データが変更されていない必要があります。データが変更されている場合は、バージョン番号を増やす必要があります。
10. JUCの共通クラス(java.util.concurrent)
同時: 同時実行 (マルチスレッド)
1、コラブルインターフェース
スレッドを立てる方法でもあります
Runnable はタスク (実行メソッド) を表すことができ、void を返します。
Callable はタスク (呼び出しメソッド) を表し、特定の値を返すこともでき、その型はジェネリック パラメーターで指定できます。
マルチスレッド操作を実行している場合、マルチスレッド実行のプロセスのみを考慮する場合は、Runnable を使用します。
スレッド プールやタイマーと同様に、これらはプロセスにのみ関連しており、Runnable を使用します。
マルチスレッドの計算結果が気になる場合は、Callable を使用する方が適切です。
マルチスレッドで数式を計算するには、Callable を使用する方が適切です。
static class Result {
public int sum = 0;
public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result();
Thread t = new Thread() {
@Override
public void run() {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
synchronized (result.lock) {
result.sum = sum;
result.lock.notify();
}
}
};
t.start();
synchronized (result.lock) {
while (result.sum == 0) {
result.lock.wait();
}
System.out.println(result.sum);
}
}
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo27 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1;i <= 1000;i++){
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
Integer result = futureTask.get();
System.out.println(result);
}
}
Callable を使用する場合、Thread のコンストラクター パラメーターを直接使用することはできません。
補助クラスFutureTaskを使用できます。
それでは、この結果はいつ計算できるのでしょうか? この問題は、FutureTask を使用することで解決できます。
麻辣タンを食べに行くときと同じように、お金を払ったら上司からレシートが渡されるので、麻辣タンが完成したらそのレシートを使って食事を受け取ることができます。ここではFutureTaskを使用して結果を取得します。
2、リエントラントロック
lock() ロックアンロック() アンロック
ReentrantLock には、synchronized にはない機能がいくつかあります。
1. ロック用の tryLock メソッドを提供します
ロック操作の場合、ロックが失敗した場合はブロックして待機します (死ぬまで待機します)。
trylockの場合、ロックに失敗した場合は直接falseを返す/待ち時間も設定可能
tryLock は、ロック操作のためにより多くの操作スペースを提供します
2. ReentrantLock には 2 つのモードがあり、公平なロック状態または不公平なロック状態で動作します。
コンストラクターのパラメーターを介して設定できる公平/不公平モード
3. ReentrantLock には待機通知メカニズムもあり、Condition のようなクラスで完了できます。
ここでの待機通知は、待機通知よりも強力です。
これらは ReentrantLock の利点です
しかし、ReentranLock の欠点も明らかです: ロック解除は見逃しやすいため、ロック解除を実行するために Final を使用できます。
同期ロック オブジェクトは任意のオブジェクトであり、ReentrantLock ロック オブジェクトはそれ自体です。
つまり、複数のスレッドが異なる ReentrantLock のロック メソッドを呼び出した場合、ロックの競合は発生しません。
実際の開発では、マルチスレッド開発を実行してロックを使用する場合、依然として同期が第一の選択肢となります。
3. アトミッククラス
内容のほとんどは以前に議論されたものです
アトミック クラスの適用シナリオは何ですか?
(1) 計数要件
再生数、いいね数、コイン数、転送数、コレクション数...
同じ動画を多くの人が同時に再生/いいね/お気に入りに入れています...
(2) 統計的効果
アトミック クラスを使用して、エラーのあるリクエストの数をカウントします。
エラーのあるリクエストの数を記録し、別の監視サーバーを作成し、表示サーバーからこれらのエラー数を取得し、グラフを通じてページ上に描画します。
プログラムのリリース後にエラーの数が突然大幅に増加した場合は、このバージョンのコードにバグが含まれている可能性が高いことを意味します。
4.セマフォ_
セマフォはオペレーティング システムにもよく登場します
セマフォは同時プログラミングにおける重要な概念/コンポーネントです
正確に言うと、セマフォは利用可能なリソースと重要なリソースの数を記述するカウンタ (変数) です。
クリティカルリソース: 複数のプロセス/スレッドおよび他の同時実行エンティティによって共通に使用できるリソース (複数のスレッドが同じ変数を変更するため、この変数はクリティカルリソースと見なすことができます)
現在のスレッドに利用可能な重要なリソースがあるかどうかを示します。
駐車場の入り口には、通常、「駐車場には XX 台の空きスペースがあります。このデータがセマフォであり、空いている駐車スペースが利用可能なリソースです」という表示があります。
駐車場に車で入る場合は、駐車スペースの申請(利用可能なリソースの申請)に相当しますが、このときカウンタは-1されます。これをP操作、取得(申請)と呼びます。
駐車場から車で出ると、駐車スペースを解放する(空きリソースを解放する)ことに相当し、このときカウンタが+1されることをV操作解放といいます。
カウンタが 0 の場合、P 操作の実行を続けるとブロックされ、他のスレッドが V 操作を実行してアイドル状態のリソースが解放されるまで待機します。
このブロックと待機のプロセスはロックに似ています
ロックは本質的に特別なセマフォです (内部の値は 0 または 1 です)。
セマフォはロックよりも一般的であり、1 つのリソースだけでなく N 個のリソースも記述することができます。
概念的にはより広範ですが、実際の開発ではさらに多くのロックが存在します (バイナリ セマフォ シナリオの方が一般的です)。
5、カウントダウンラッチ
特定のシナリオ用のコンポーネント
例: 何かをダウンロードする
比較的大きなファイルのダウンロードが比較的遅くなる場合があります (自宅の速度制限のせいではなく、サーバーの制限が原因であることがよくあります)
大きなファイルをいくつかの小さな部分に分割し、複数のスレッドを使用してダウンロードするマルチスレッド ダウンローダーがいくつかあります。
各スレッドはパーツのダウンロードを担当し、各スレッドはネットワーク接続です。
ダウンロード速度を大幅に向上できる
タスクを完了するために複数のタスクに分割する必要がある場合、複数のタスクが現在完了したかどうかをどのように測定すればよいでしょうか?
このとき、CountDownLatch を使用できます。
import java.util.concurrent.CountDownLatch;
//CountDownLatch
public class Demo29 {
public static void main(String[] args) throws InterruptedException {
//构造方法中,指定要创建几个任务
CountDownLatch countDownLach = new CountDownLatch(10);
for(int i = 0;i < 10;i ++){
int id = i;
Thread t = new Thread(() ->{
System.out.println("线程" + id + " 开始工作");
try {
//使用 sleep 代指某些耗时操作
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程" + id + " 结束工作");
//在每个任务结束这里,调用一下方法
//把 10 个线程想象成短跑比赛的 10 个运动员,countDown 就是运动员撞线了
countDownLach.countDown();
});
t.start();
}
//主线程如何知道上述所有任务是否都完成了呢?
//难道要在主线程中调用 10 次 join 吗?
//万一要是这个任务结束,但是线程不需要结束,join 不也就不行了吗
//主线程中可以使用 countDownLatch 负责等待任务结束
// a -> all,等待所有任务结束,当调用 countDown 次数 < 初始设置的次数,await 就会阻塞
countDownLach.await();
System.out.println("多个线程的所有任务都执行完毕");
}
}
11. コレクションクラス
コレクション クラスのうち、スレッドセーフなものはどれですか?
(1) 複数のスレッドがある場合、同期の制限により、セット操作の同時実行はスレッドセーフになります。(2) 複数のスレッドが、get の値が xxx であると判断してから set を実行するなど、より複雑な操作を実行する場合、そのような操作はスレッドセーフではない可能性があります。
(1) ArrayListを複数環境で利用する場合
Collections.synchronizedList(new ArrayList);
ArrayList 自体は synchronized を使用しません
ただし、自分でロックしたくない場合は、上記のものを使用できます
ArrayList を Vector のように動作させる (めったに使用されない)
読み取りに複数のスレッドが使用される場合、スレッドの安全性の問題は発生しません。
スレッドがスレッドを変更すると、スレッド自体のコピーが作成されます。
特に変更に時間がかかる場合は、他のスレッドが古いデータから読み取ることになります。
変更が完了したら、新しい ArrayList を使用して古い ArrayList を置き換えます (基本的に参照の再割り当てであり、非常に高速でアトミックです)。
このプロセスでは、ロック操作は導入されません。
スレッドセーフな変更は、コピーの作成 --> コピーの変更 --> コピーを使用して置き換えるというプロセスを使用して完了します。
(2)キューを使用したマルチスレッド環境
(3)マルチスレッド環境ではハッシュテーブルを使用する
(a)ハッシュテーブル
HashMap はスレッドセーフではありません
HashTable はスレッドセーフであり、主要なメソッドは同期されます。
h オブジェクトに対する読み取り操作には、このオブジェクトのロックが含まれます。
ハッシュテーブルでは、ハッシュ関数でキーを計算し、最終的に配列の添字を取得します。最終的にkey1とkey2の数値を取得すると、
具体的な方法は、リンクリストごとにロックを設定することです。
ハッシュ テーブルには多数のリンク リストがあり、2 つのスレッドがたまたま同じリンク リストを同時に操作する可能性は非常に低いです。
により、全体的なロックのオーバーヘッドが大幅に削減されます。
同期されたオブジェクトはどれもロックに使用できるため、各リンク リストのヘッド ノードをロック オブジェクトとして単純に使用できます。
(b)ConcurretnhashMap
ConcurrentHashMap の改善:
1. ロックの粒度が減少します。各リンク リストには 1 つのロックがあります。ほとんどの場合、ロックの競合は発生しません [コア]
2. CAS 操作は広く使用されており、size++ も同時に実行されますが、このような操作によってロックの競合が発生することはありません。
3. 書き込み操作はリンク リスト レベルでロックされますが、読み取り操作はロックされません。
4. 拡張操作に最適化: プログレッシブ拡張
HashTable の展開がトリガーされると、すべての要素の転送がすぐに一度に完了しますが、このプロセスには非常に時間がかかります。
プログレッシブ拡張: 複数の部分に分割します。拡張が必要な場合は、別のより大きな配列が作成され、その後、古い配列のデータが新しい配列に徐々に移動されます。一定の期間、古い配列が配列に保存されます。新しい配列が同時に存在します
1. 新しい要素を追加し、新しい配列に挿入します。
2. 要素を削除するには、古い配列要素を削除するだけです。
3. 要素を見つけるには、新しい配列と古い配列の両方を検索する必要があります。
4. 要素を変更し、この要素を新しい配列に均一に配置します。
同時に、各操作はある程度の転送をトリガーします。少しずつ転送するたびに、全体の時間がそれほど長くならないようにすることができます。少し加算すると、転送は徐々に完了し、古いアレイは完全に破棄できます。
Java8 より前では、ConcurrentHashMap はセグメント化されたロックを使用していましたが、Java8 以降では、リンクされたリストごとに 1 つのロックが存在します。
セグメント化されたロックを使用すると効率が向上しますが、リンクされたリストごとに 1 つのロックを使用するほど効率が良くなく、コードの実装がより複雑になります。