【2023年】Javaマルチスレッドの基礎と共通スレッドクラス

1. プロセスとスレッドの基本概念

1. プロセスの背景

当初のコンピュータは特定の命令しか受け付けず、ユーザーが命令を入力するたびにコンピュータが動作を実行していました。ユーザーが考えたり入力したりしている間、コンピューターは待機しています。これは非常に非効率であり、多くの場合、コンピュータは待機状態になります。

バッチオペレーティングシステム

その後、リストを作成するために必要な操作を必要とする一連の命令を書き留め、それを一度にコンピューターに渡すバッチ処理オペレーティング システムが登場しました。ユーザーは、実行する必要のある複数のプログラムをテープに書き込み、それをコンピュータに送信して、これらのプログラムを 1 つずつ読み取って実行し、出力結果を別のテープに書き込みます。

バッチオペレーティングシステムはコンピュータの効率をある程度向上させますが、バッチオペレーティングシステムの命令動作モードは依然としてシリアルであるため、常に1つのプログラムのみがメモリ内で実行され、後続のプログラムは実行を待つ必要があります。実行が開始される前に、I/O 操作やネットワークなどの理由で前のプログラムがブロックされる場合があり、バッチ処理の操作効率は高くありません。

プロセスのご提案

コンピュータのパフォーマンスに対する人々の要求はますます高くなっており、既存のバッチ オペレーティング システムでは人々のニーズを満たすことができません。バッチ オペレーティング システムのボトルネックは、メモリ内にプログラムが 1 つしかないことですが、メモリ内に複数のプログラムを存在できるのでしょうか? これは人々が緊急に解決する必要がある問題です。

そこで、科学者たちはプロセスという概念を提案しました。

プロセスとは、アプリケーションによってメモリ内に割り当てられた領域、つまり実行中のプログラムであり、各プロセスは互いに干渉しません。同時に、プロセスは実行中のプログラムのステータスを各瞬間に保存します。

プログラム: 特定のタスクや機能を実行できる、特定のプログラミング言語 (Java、Python など) で書かれたコードの集合。命令とデータの順序付けられた集合であり、静的コードの一部です。

このとき、CPU はタイム スライス ローテーションを使用してプロセスを実行します。CPU は、各プロセスにタイム スライスと呼ばれる期間を割り当てます。タイム スライスの終了時点でプロセスがまだ実行中の場合、プロセスの実行は一時停止され、CPU が別のプロセスに割り当てられます (このプロセスはコンテキスト スイッチングと呼ばれます)。タイム スライスが終了する前にプロセスがブロックまたは終了した場合、CPU はタイム スライスが終了するのを待たずにすぐに切り替わります。

プロセスが一時停止されると、現在のプロセスの状態 (プロセス ID、プロセスが使用するリソースなど) が保存され、次回スイッチバックするときに以前に保存された状態に従って復元され、実行が継続されます。

プロセス + CPU タイム スライス ローテーション方式を使用するオペレーティング システムは、巨視的には同じ時間内に複数のタスクを実行しているように見えます。言い換えれば、プロセスによってオペレーティング システムの同時実行が可能になります。マクロの観点から見ると、同時実行では複数のタスクが実行されますが、実際には、シングルコア CPU の場合、特定の時点で CPU リソースを占有しているのは 1 つのタスクだけです。

オペレーティング システムの要件はさらに増加し​​ています

プロセスの登場によりオペレーティング システムのパフォーマンスは大幅に向上しましたが、時間が経つにつれて、プロセスが一定期間に 1 つのことしか実行できないことに人々は満足しなくなりました。これらのサブタスクは効率に大きく影響します。

たとえば、ウイルス対策ソフトウェアがユーザーのコンピュータを検出するときに、特定のテストでスタックすると、後続のテスト項目にも影響が及びます。つまり、ウイルス対策ソフトのウイルススキャン機能を使用する場合、ウイルススキャンが完了するまでウイルス対策ソフトのゴミ駆除機能を使用することはできず、明らかに人々の要求を満たすことができません。

スレッド提案

では、これらのサブタスクは同時に実行できるのでしょうか? そこで人々は、1 つのスレッドがサブタスクを実行できるようにするスレッドの概念を提案しました。これにより、プロセスに複数のスレッドが含まれ、各スレッドが個別のサブタスクを担当します。

スレッドを使用すると、作業がはるかに簡単になります。ユーザーがウイルススキャン機能を使用する場合、ウイルススキャンスレッドを実行させます。同時に、ユーザがガベージクリーニング機能を再び使用する場合、ウイルススキャンスレッドを最初に一時停止し、ユーザのガベージクリーニング操作に最初に応答してガベージクリーニングスレッドを実行することができる。応答が完了したら、切り替えてウイルス スレッドのスキャンを実行します。

注: オペレーティング システムが各スレッドにタイム スライスを割り当てる方法には、スレッド スケジューリング戦略が関係します。興味のある学生は、「オペレーティング システム」を参照してください。この記事では詳細な説明は行いません。

つまり、プロセスとスレッドの導入により、オペレーティング システムのパフォーマンスが大幅に向上しました。プロセスはオペレーティング システムの同時実行を可能にし、スレッドはプロセスの内部同時実行を可能にします。

同時実行性はマルチ処理によっても実現できます。なぜマルチスレッドを使用するのでしょうか?

マルチプロセス モードでは確かに同時実行性を実現できますが、マルチスレッドを使用すると次のような利点があります。

  • プロセス間の通信はより複雑ですが、スレッド間の通信は比較的単純で、通常はスレッド間の通信が容易な共有リソースを使用する必要があります。
    プロセスは重量級ですが、スレッドは軽量であるため、マルチスレッドのシステム オーバーヘッドは小さくなります。

プロセスとスレッドの違い

プロセスは独立した実行環境であり、スレッドはプロセス内で実行されるタスクです。それらの本質的な違いは、別個のメモリ アドレス空間と他のシステム リソース (I/O など) を占有するかどうかです。

  • プロセスは特定のメモリ アドレス空間を単独で占有するため、プロセス間でメモリが分離され、データが分離され、データ共有は複雑ですが同期は簡単で、各プロセスは互いに干渉しません。スレッドはメモリ アドレス空間とスレッドが占有するリソースを共有します。プロセスとデータの共有は簡単ですが、同期は複雑です。

  • プロセスは特定のメモリ アドレス空間を単独で占有し、あるプロセスで問題が発生しても他のプロセスには影響せず、メイン プログラムの安定性にも影響を与えないため信頼性が高く、スレッドのクラッシュはプログラム全体の安定性に影響を与える可能性があります。信頼性が低い。

  • プロセスは単独で特定のメモリ アドレス空間を占有します。プロセスの作成と破棄には、レジスタとスタック情報の保存が必要なだけでなく、リソースの割り当てとリサイクル、およびページ スケジューリングも必要になりますが、これは比較的高価です。スレッドはレジスタとスタックを保存するだけで済みます。情報は比較的少ないです。

もう 1 つの重要な違いは、プロセスがオペレーティング システムによるリソース割り当ての基本単位であるのに対し、スレッドはオペレーティング システムによるスケジューリングの基本単位、つまり CPU 割り当て時間の単位であることです。

2. タイトルコンテキストの切り替え

コンテキスト スイッチ (プロセス スイッチまたはタスク スイッチとも呼ばれる) は、CPU があるプロセス (またはスレッド) から別のプロセス (またはスレッド) に切り替わることです。コンテキストとは、ある時点での CPU レジスタおよびプログラム カウンタの内容を指します。

レジスタは CPU 内の少量の高速フラッシュ メモリであり、通常、コンピュータ プログラムの実行速度を向上させるために、計算プロセスの中間値を保存およびアクセスします。

プログラム カウンタは、命令シーケンス内で実行中の CPU の位置を示すために使用される特殊目的のレジスタです
。格納される値は、実行中の命令の位置、または次に実行される命令の位置です。特定の実装によって異なります。特定のシステム上で。

ねじA~Bの例

1. まずスレッド A を一時停止し、その状態を CPU のメモリに保存します。

2. 次のスレッド B のコンテキストをメモリから取得して CPU レジスタに復元し、B スレッドを実行します。

3. B の実行が終了したら、プログラム カウンタが指す位置に従ってスレッド A を再開します。

CPU は、各スレッドに CPU タイム スライスを割り当てることでマルチスレッド メカニズムを実装します。CPU はタイム スライス割り当てアルゴリズムを通じてタスクを周期的に実行し、現在のタスクがタイム スライスを実行した後、次のタスクに切り替えます。

ただし、前のタスクのステータスは切り替える前に保存されるため、次回このタスクに切り替えるときに、このタスクのステータスを再度ロードできます。したがって、タスクの保存から再ロードまでのプロセスはコンテキストの切り替えです。

コンテキストの切り替えは通常、大量の計算を必要とします。つまり、この操作は多くの CPU 時間を消費するため、スレッドが多いほど必ずしも良いとは限りませんシステム内のコンテキスト スイッチの数を減らす方法は、マルチスレッドのパフォーマンスを向上させるための重要な問題です。

2. Java マルチスレッド エントリ クラスとインターフェイス

返品不可

まず、「スレッド」クラスが必要です。JDK は、独自の「スレッド」クラスを実装できるようにする Thread クラスと Runnable インターフェイスを提供します。

  1. Thread クラスを継承し、run メソッドをオーバーライドします。
  2. Runnable インターフェースの run メソッドを実装します。
  • スレッドクラス
public class Demo {
    
    
    public static class MyThread extends Thread {
    
    
        @Override
        public void run() {
    
    
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {
    
    
        Thread myThread = new MyThread();
        myThread.start();
    }
}

start() メソッドが呼び出されるまでスレッドは開始されないことに注意してください。

プログラム内で start() メソッドを呼び出した後、仮想マシンはまずスレッドを作成し、スレッドが初めてタイム スライスを取得するときに run() メソッドを呼び出します。

start() メソッドを複数回呼び出すことはできないことに注意してください。初めて start() メソッドを呼び出した後、再度 start() メソッドを呼び出すと IllegalThreadStateException がスローされます。

  • 実行可能なインターフェース
public class Demo {
    
    
    public static class MyThread implements Runnable {
    
    
        @Override
        public void run() {
    
    
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {
    
    

        new Thread(new MyThread()).start();

        // Java 8 函数式编程,可以省略MyThread类
        new Thread(() -> {
    
    
            System.out.println("Java 8 匿名内部类");
        }).start();
    }
}

リターンもあるよ

Callable、Future、FutureTask
一般に、新しいスレッドを作成するには Runnable と Thread を使用します。ただし、run メソッドには戻り値がないという欠点があります。タスクを実行するためにスレッドを開始し、タスクの完了後に戻り値を取得したい場合があります。

JDK は、この問題を解決するために、Callable インターフェイスと Future インターフェイスを提供します。これは、いわゆる「非同期」モデルでもあります。

  • 呼び出し可能なインターフェース
// 自定义Callable
class Task implements Callable<Integer>{
    
    
    @Override
    public Integer call() throws Exception {
    
    
        // 模拟计算需要一秒
        Thread.sleep(1000);
        return 2;
    }
    public static void main(String args[]) throws Exception {
    
    
        // 使用
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        Future<Integer> result = executor.submit(task);
        // 注意调用get方法会阻塞当前线程,直到得到结果。
        // 所以实际编码中建议使用可以设置超时时间的重载get方法。
        System.out.println(result.get());
    }
}
  • Future インターフェイス
    Future インターフェイスには、比較的単純なメソッドがいくつかあるだけです。

cancelこの方法では、スレッドの実行をキャンセルしてみます。

キャンセルしようとしても、キャンセルが成功しない場合があることに注意してくださいタスクは完了、キャンセル、またはその他の要因によりキャンセルできない場合があるため、キャンセルに失敗する可能性があります。booleanこの型の戻り値は「キャンセルが成功したかどうか」を意味します。このパラメータは、paramBooleanスレッドの実行を中断してキャンセルするかどうかを示します。

したがって、場合によっては、タスクをキャンセルできるようにするために、Callable代わりにそれが使用されますRunnableキャンセル可能にするために使用したがFuture、使用可能な結果を​​提供しない場合は、Future<?>正式な型を宣言して、基礎となるタスクの結果として null を返すことができます。

- FutureTask クラス

// 自定义Callable,与上面一样
class Task implements Callable<Integer>{
    
    
    @Override
    public Integer call() throws Exception {
    
    
        // 模拟计算需要一秒
        Thread.sleep(1000);
        return 2;
    }
    public static void main(String args[]) throws Exception {
    
    
        // 使用
        ExecutorService executor = Executors.newCachedThreadPool();
        FutureTask<Integer> futureTask = new FutureTask<>(new Task());
        executor.submit(futureTask);
        System.out.println(futureTask.get());
    }
}

おすすめ

転載: blog.csdn.net/weixin_52315708/article/details/131522011