Java マルチスレッド シリーズ IV (シングル ケース モード + ブロッキング キュー + タイマー + スレッド プール)


1. デザインパターン(シングルケースモード+ファクトリーモード)

デザイン パターンはソフトウェア開発における「チェス ゲーム」であり、ソフトウェア開発には多くの一般的な「問題シナリオ」があります。これらの問題シナリオに対応して、大手企業はいくつかの固定ルーチンをまとめました。これらの規則に従ってコードを実装するのは良くないかもしれませんが、少なくとも悪くはありません。この段階では、次の 2 つの設計パターンをマスターする必要があります。(1) シングルトンモード (2) ファクトリーモード

1. シングルトンモード

概念/機能: シングルトン パターンは、複数のインスタンスを作成することなく、プログラム内に特定のクラスのインスタンスが 1 つだけ存在することを保証します。

Java でシングルトン パターンを実装する方法は数多くありますが、基本的には同じです。ここでは主に 2 つのタイプを紹介します。ハングリーモードそしてレイジーモード

コンピュータにおいて「怠惰」は褒め言葉として使われることが多く、実際の意味での怠惰ではなく、ある種の「冷静さ」を強調している場合が多いです。そして「空腹」はある種の「切迫感」を強調します。

たとえば、このシナリオの「Hungry Man」モードでは、コンピュータがハードディスク ファイルを読み取って表示すると、ファイルのすべての内容がメモリに読み込まれて一緒に表示されます。このモードでは、ファイルが非常に大きい場合、メモリ不足や表示の遅れなどの問題が発生する可能性があります。反対に、遅延モードの場合: 一度にすべてを読み取るのではなく、一度に一部のみを読み取ります。現在の画面が最初に表示されます。後続のページが切り替わってファイルの内容を読み取り続ける場合、このモードでは大幅に時間がかかります。効率とユーザーエクスペリエンスを向上させます。

Java のマルチスレッドにおけるシングルトン モードでは、Java 構文を使用して、特定のクラスが 1 つのインスタンスのみを作成し、複数回新規作成できないようにすることができます。具体的な実装は次のとおりです: (1) Hungry Man モードのコード
実装
:

// 饿汉模式实现单例
class Singleton {
    
    
    // 唯一实例的本体
    private static Singleton instance = new Singleton();

    // 获取到实例的方法
    public static Singleton getInstance() {
    
    
        return instance;
    }

    // 禁止外部 new 实例(将构造方法私有化:类内可以使用,类外不能使用)
    private Singleton() {
    
     }
}
public class Test {
    
    
    public static void main(String[] args) {
    
    
        // 使用
        Instance instance = Instance.getInstance();
        
        //由于设置了私有的构造方法,所以这样写会报错
        //Instance instance1 = new Instance();
    }
}

例証します:

  1. private static Singleton instance = new Singleton(); をここで変更しますstaticこの属性はクラスの属性です jvm ではクラスの属性は 1 つだけなので、このメンバーはクラスの客観性において当然一意です。
  2. Singleton クラスが新しいオブジェクトを作成し続けるのを防ぐには、コンストラクター メソッドをプライベート化する必要があります。これにより、クラス内でのインスタンスの作成が許可され、外部でのインスタンスの再作成が禁止されます。ここで特に注意が必要なのは、このprivateコンストラクターはプライベート化されていますが、クラス内で気軽に使用できることです。
  3. 現在の変数インスタンスは静的であり、割り当てはクラスのロード段階で完了するため、マルチスレッド下で静的メソッド getInstance を呼び出して取得される唯一のインスタンスは読み取り操作のみであり、それ自体はスレッドセーフです。

(2) 遅延モード
コードの実装:

// 懒汉模式实现单例
class SingletonLazy {
    
    
    volatile private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
    
    
        // 这个条件, 判定是否要加锁. 如果对象已经有了, 就不必加锁了, 此时本身就是线程安全的.
        if (instance == null) {
    
    
            synchronized (SingletonLazy.class) {
    
    
                if (instance == null) {
    
    
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
	// 构造方法私有化
    private SingletonLazy() {
    
     }
}
public class Test {
    
    
    public static void main(String[] args) {
    
    
        // 使用
        Instance2 I1 = Instance2.getInstance2();
    }
}

遅延モードではシングルトンが実装されると上で説明しましたが、後続のマルチスレッドでシングルトンを使用する場合、「読み取り」操作のみが含まれるため、スレッドセーフです。ただし、Hungry Pattern でシングルトンを実装する場合、新しいオブジェクトを作成する必要はなく、シングルトンは使用されたときにのみ作成されるため、読み取りと書き込みが伴います。マルチスレッドでは、このモードではスレッド セーフティの問題が発生する可能性があるため、上記のコードでスレッド セーフティ ロジックを確認できます。スレッド セーフティを確保するために上記のコードで使用されているコード ロジックを詳しく説明します。

(1) 同期ロックはif 判断和 newアトミックな操作であることが保証されます。

コードがロックされていない場合、スレッドのランダムなスケジューリングにより、if 判定後に各スレッドが切断される可能性が高く、複数のインスタンスの作成はおろか、複数のインスタンスの作成につながる可能性があります。シングルトンパターンを満たしていません。オブジェクトが実際にメモリ空間を占有する必要があることはわかっていますが、各オブジェクトが非常に大きなメモリ空間を占有する場合、N 個のスレッドが N 個のオブジェクトを作成する可能性があり、プログラムが過負荷になる可能性があります。

(2) 最適化:二層if判定を使用

上記のロックされたコードは次のとおりです。

synchronized (SingletonLazy.class) {
    
    
    if (instance == null) {
    
    
        instance = new SingletonLazy();
    }
}
return instance;

ロックは if 判定と new のアトミック性を保証しますが、この種のロックにはまだ欠陥があります。if 判定操作は同期ロック内に配置されるため、シングルトンが作成されているかどうかに関係なく、いつでも getInstance が呼び出されます。ロックの競合を引き起こし、スレッドのブロックを引き起こします。さらに、ロックは比較的高価な操作であり、ロックを繰り返すとプログラムの実行効率が低下します。

分析後、上記のスレッドのセキュリティの問題は、オブジェクトが初めて作成されるときにのみ発生すると簡単に結論付けることができます。新しいオブジェクトの準備が完了すると、その後の getInstance の呼び出しは単純な読み取り操作になります。インスタンスを直接 ruturn することができます。必要ありません。ロックされています。したがって、if 条件判断の層を追加できます。instance == null

if (instance == null) {
    
    
    synchronized (SingletonLazy.class) {
    
    
    	if (instance == null) {
    
    
            instance = new SingletonLazy();
        }
    }
}
return instance;

ロックの外層にロック条件を設定すると、instance==nullインスタンスが作成されていないときにロックがロックされます。インスタンスが作成されると、後続のスレッドは、instance= 条件に遭遇したときに、作成されたスレッドを直接返します。 =null as false. シングルトンは再度ロックされません。このとき、このプログラムはマルチスレッド下での複数の不要なロックを回避し、プログラムのオーバーヘッドを削減します。

(3) 命令の再配置を防ぐために volatile を使用する

volatile private static SingletonLazy instance = null;

volatile キーワードを使用しない場合、新しいオブジェクトを作成するときに並べ替え (オブジェクト作成プロセス中の変数代入や参照関係の確立などの操作の順序が調整される) が発生し、スレッドがオブジェクト参照を認識する可能性があります。 null 以外の値ですが、オブジェクトは実際には完全に初期化されていません。これにより、プログラムで予期しない動作やエラーが発生する可能性があります。

volatile キーワードを使用すると、命令の並べ替えを禁止し、インスタンスがいつでも一意で予期されるものになるようにすることで、オブジェクト作成時のスレッド セーフティの問題を回避できます。

シングルトン モードのスレッド セーフティの問題

  1. ハングリー モード: 当然安全な読み取り専用操作
  2. 遅延モード: スレッドは読み取りと書き込みの両方で安全ではありません
    (1) ロック、if と new をアトミック操作に変換
    (2) 二重層の if、不必要なロック操作を削減
    (3) volatile を使用して命令の並べ替えを禁止し、後続のスレッドが確実に完全なオブジェクトを確実に取得します。

2. ファクトリーモード

ファクトリ パターンは、構築メソッドの欠点を補うために使用されます。構築メソッドがさまざまな角度から構築を実装したい場合、メソッドのオーバーロードのみに依存できますが、メソッドのオーバーロードは、構文のせいでシナリオによってはあまり適していません。制限。

たとえば、平面上の点を表現したい場合、2 つの表現方法があります。1 つは平面座標で、もう 1 つは極座標です。

class Point {
    
     
	public Point(double x , double y) {
    
    }
	public Point(double r,double a) {
    
    }
}

コンストラクター メソッドがオーバーロードされている場合、上記の 2 つのメソッドはまったく同じであるため、オーバーロードを構成できないことが判明したため、ファクトリ パターンが導入されます (Point クラスをカプセル化する新しいファクトリ クラスを作成します)。

class PointBuild {
    
    
	public static Point planar(double x,double y){
    
    ...}
	public static Point polar(double r,double a){
    
    ...}
}

このとき、メソッド名が異なるため、上記の2つの座標を表現できない問題は解決される。

2. キューのブロック

ブロッキング キュー、つまりブロッキングのあるキューは、キューの基本プロパティである先入れ先出しを満たします。そして次のような特徴を持っています。

  1. キューがいっぱいの場合、キューを続行すると、別のスレッドがキューから要素を取得するまでブロックされます。
  2. キューが空の場合、デキューを続行すると、他のスレッドが要素をキューに挿入するまでブロックされます。
  3. に基づいて12ブロッキング キューをスレッド セーフなデータ構造にすることができます

マルチスレッド コードを作成する場合、複数のスレッド間のデータ対話のために、ブロッキング キューを使用してコードの作成を簡素化できます。さらに重要なのは、次のような典型的なアプリケーション シナリオがあることです。生産者消費者モデルこれは非常に典型的な開発モデルです。

1. 生産者・消費者モデル

生産者と消費者の間でデータをやり取りするには、必要に応じて取引場所が使用されますが、この取引場所が「ブロッキングキュー」です。

プロデューサーとコンシューマーは互いに直接通信するのではなく、ブロッキング キューを介して通信します。したがって、プロデューサーがデータを生成した後、コンシューマーがデータを処理するのを待つ必要はなく、データを直接ブロッキング キューにスローします。プロデューサーにデータを要求するのではなく、ブロッキング キューから直接データを取得します。

レストランのウェイターシェフ:

レストランでは、ウェイターとシェフは生産者-消費者モデルの典型的な例です。ウェイターは消費者であり、顧客から注文情報を取得してシェフに渡す責任があります。シェフはプロデューサーであり、ウェイターから提供された注文情報に基づいて料理を作り、ウェイターに料理を渡し、ウェイターは料理を顧客に届けます。

プロセス全体を通じて、ウェイターとシェフは点餐单共有プラットフォームを通じて仕事を調整し、ウェイターは顧客の注文ニーズを把握して注文リストに追加する責任を負い、シェフは料理を作り、顧客のニーズに従う責任を負います。注文リスト。調理済みの食品を準備して、ウェイターが持ち帰ることができるようにカウンターに置きます。この注文フォームはブロッキング キューに似ています。

2. 生産者と消費者間のブロッキングの役割

(1) キューをブロックすると、プロデューサーとコンシューマーが切り離される可能性があります。
コードを記述するときの一般的な要件は、「凝集度が高く、結合度が低いこと」です。いわゆる結合度とは、2 つのモジュール間の関連度を指します。相関が強いことを「高結合」、相関が低いことを「低結合」と呼びます。コードを記述するときは、全体への影響を避けるために低結合を追求してください。凝集度は、モジュール内のコンポーネント間の相関度を指し、凝集度が低いとは、関連するコードがまとめられておらず、乱雑であることを意味します。凝集性の高い、関連するコードはカテゴリーごとに規制されます。

プロデューサー/コンシューマー モデルでは、キューをブロックすることでプロデューサーとコンシューマー間の結合を減らすことができます。たとえば、現在 3 つのサーバー A、B、C があります。A はエントランス サーバー (単純なサ​​ービスを受け入れて処理するため、ハングしにくい)、B と C はビジネス サーバー (複雑なサービスを処理し、ハングしやすい) です。 )。

(2) ブロッキングキューはバッファに相当し、生産者と消費者の処理能力のバランスをとり、山を削り、谷を埋める役割を果たします。

たとえば、「フラッシュ セール」シナリオでは、サーバーは同時に多数の支払いリクエストを受信する可能性があります。これらの支払いリクエストが直接処理される場合、サーバーはそれを処理できない可能性があります (各支払いリクエストの処理には比較的複雑なプロセスが必要です)。現時点では、これらのリクエストをブロッキング キューに入れることができます (ブロッキング キューには用事がなく、コードは安定しており、ハングしにくいです)。その後、コンシューマ スレッドがブロッキング キューからリクエストを取得して、それぞれのリクエストを処理します。通常料金でのお支払いをお願い致します。これにより、効果的に
「ピークをカット」し、突然のリクエストの波によってサーバーが直接過負荷になるのを防ぐことができます。

「フラッシュセール」シナリオ後にトラフィックが減少して谷に達した場合でも、コンシューマスレッドはブロッキングキューから以前に圧縮されたリクエストを元のレートで取得して処理できるため、処理プロセス全体がよりスムーズになります。

3. 標準ライブラリのブロッキング キューを使用して、プロデューサー/コンシューマー モデルを実装します。

ブロッキング キューは Java 標準ライブラリに組み込まれています。一部のプログラムでブロッキング キューを使用する必要がある場合は、標準ライブラリ内のキューを直接使用できます。

  1. BlockingQueueこれはインターフェイスであり、実際に実装されるクラスは LinkedBlockingQueue です。
  2. putこのメソッドは、エンキューのブロックとtakeデキューのブロックに使用されます。
  3. BlockingQueue にはofferpollpeekなどのメソッドもありますが、これらのメソッドにはブロッキング特性はありません。
public static void main(String[] args) {
    
    
        // 使用Java库中的阻塞队列:
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();

        //消费者
        Thread t1 = new Thread(()->{
    
    
           while (true) {
    
    
               try {
    
    
                   int value = blockingQueue.take();
                   System.out.println("消费元素:"+value);
               } catch (InterruptedException e) {
    
    
                   e.printStackTrace();
               }
           }
        });

        //生产者
        Thread t2 = new Thread(()->{
    
    
            int value = 0;
            while (true) {
    
    
                try {
    
    
                    blockingQueue.put(value);
                    System.out.println("生产元素:"+value);
                    value++;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });

        t1.start();
        t2.start();
        // 上述代码, 让生产者, 每隔 1s 生产一个元素。
        // 让消费者则直接消费, 不受限制。
        // 所以可能会看到,生产者生产1个元素就消费者就消费1个元素,消费完就阻塞,等待1s生产出新的元素后再唤醒消费者。

    }

4. ブロッキングキューをシミュレートする

  1. 通常のキューを実装する
  2. スレッドの安全性もプラス
  3. ブロック機能を追加
public class MyBlockingQueue {
    
    
    private int[] items = new int[1000];
    // 约定 [head, tail) 队列 的有效元素
    volatile private int head = 0;
    volatile private int tail = 0;
    volatile private  int size = 0;

    // 入队列
    synchronized public void put(int value) throws InterruptedException {
    
    
        while (size == items.length) {
    
    
            // 队列为满,阻塞等待
            this.wait();
        }
        // 以下是不为满的情况:
        // 插入元素
        items[tail] = value;
        tail++;
        // 判断 tail 大小,如果 tail 达到末尾,需要从头开始
        if (tail == items.length) {
    
    
            tail = 0;
        }
        size++;
        // 生产元素后就唤醒阻塞
        this.notify();
    }

    //出队列
    synchronized public int take() throws InterruptedException {
    
    
        while (size == 0) {
    
    
            // 队列为空,阻塞等待
            this.wait();
        }
        // 以下是不为空的情况:
        // 取元素
        int value = items[head];
        head++;
        // 如果 head 达到末尾,需要从头开始
        if (head == items.length) {
    
    
            head = 0;
        }
        size--;
        // 消费元素后就唤醒阻塞
        this.notify();
        return value;
    }

}

コードの説明:

  1. 上記のコードでは最後に循環キューを使用していますが、ループが満杯か空かを判断するには、(1) スペースを破棄する (2) 別の変数を使用して有効な要素の数を記録する 2 つの方法があることがわかります。上記のコードでは、有効な要素の数を記録する方法を使用しています。
  2. 配列の最大長に達したときに tail または head を処理する場合、tail = tail % items.length; の代わりに tail = 0; が使用されます。これは主に後者の方が開発効率 (可読性、保守性) と実行の点で比較的効率的であるためです。 . 効率は高くないのでおすすめしません。
  3. 上記の操作puttake操作にはすべてデータの変更が含まれるため、マルチスレッドの安全性を確保するために、メソッドを直接ロックすることを選択します。同時に、マルチスレッドでのメモリの可視性と命令の取得に関する問題を防ぐために、揮発性の変更が変数に追加されます。
  4. waitJava 関係者は、他のメソッド (割り込みなど) によって待機が中断される可能性があるため、この使用を推奨していません。このとき、待ち状態が成熟する前に事前に覚醒してしまう可能性があり、実行を続けると様々な問題が発生する可能性があります。したがって、より安全な方法は、if を while に置き換えることです。これにより、事前に中断された場合でも、条件が満たされているかどうかを判断し、条件が満たされていれば下方向に実行され、そうでない場合は待ち続けます。

個人的な意見: キューをブロックすると必ずしも実行効率が向上するとは限りませんが、同時実行性は確保できます。

3. タイマー

タイマーも「目覚まし時計」と同様、ソフトウェア開発において重要なコンポーネントです。設定された時間が経過すると、指定されたタスクが実行されます。タイマーは実際の開発において非常によく使われるコンポーネントで、例えばネットワーク通信において、相手が500ms以内にデータを返さないと接続を切断し、再接続を試みます。

1. 標準ライブラリのタイマー

  1. 標準ライブラリはTimerタイマーを表すクラスを提供します。
  2. Timer クラスの中心となるメソッドは次のとおりですscheduleタイマーのタスクをスケジュールするために使用されます。
  3. スケジュールには 2 つのパラメータが含まれます。最初のパラメータは実行するタスクを指定しTimerTask、2 番目のパラメータは実行にかかる時間 (ミリ秒単位) を指定します。

TimerTask は本質的には Runnable を実装する抽象クラスであり、run メソッドをオーバーライドする必要があります。

public abstract class TimerTask implements Runnable {
    
    ...}

タイマータイマーを使用するだけです。

public class Test_Official_Timer {
    
    
    public static void main(String[] args) {
    
    
        Timer timer = new Timer();
        // 这里的TimerTask就相当于Runnable
        timer.schedule(new TimerTask() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("world1");
            }
        },1000);
        
        System.out.println("hello!");
    }
}

実行後、スレッドが終了していないことがわかりました。これは、スレッドがタイマーに組み込まれており、フォアグラウンド スレッドがプロセスの終了を妨げるためです。また、スケジュールされたタスクの実行は、時間が経過した後に実行されるタイマー内のスレッドに依存します。

2. タイマーをシミュレートする

タイマーは内部的に多くのタスクを管理できます。多くのタスクがありますが、トリガー時間は異なるため、必要なワーカー スレッドは 1 つまたはグループだけです。これらのタスクの中で最も早い時間に達するタスクが見つかるたびに、最も早いタスクが実行されます。が最初に実行され、次に午前中の 2 番目のタスクを実行します...

明らかに、現在のシナリオでは、非常に多くのタスクを管理するために優先度を持つキューが必要です。Java ライブラリによって提供される優先度キューを使用できます。PriorityQueueこれは、タイマーに必要なコア データ構造でもあります。

最終的な実装コード:

import java.util.PriorityQueue;

// MyTask表示一个任务
class MyTask implements Comparable<MyTask> {
    
    
    public Runnable runnable;
    public long time;

    // 任务构造
    public MyTask(Runnable runnable, long delay) {
    
    
        this.runnable = runnable;
        // 取当前时刻的时间戳 + delay,为了方便后续判定,使用绝对的时间戳.
        this.time = System.currentTimeMillis() + delay;
    }

    // 比较方式
    @Override
    public int compareTo(MyTask o) {
    
    
        // [任务按照时间从小到大排序]这样的写法意味着每次取出的是时间最小的元素.
        return (int)(this.time - o.time);
    }
}

class MyTimer {
    
    
    // 优先级队列是核心数据结构
    private PriorityQueue<MyTask> queue = new PriorityQueue<>();

    // 创建一个锁对象
    private Object locker = new Object();

    // 添加任务
    public void schedule(Runnable runnable, long delay) {
    
    
        // 根据参数, 构造 MyTask, 插入队列即可.
        synchronized (locker) {
    
    
            MyTask myTask = new MyTask(runnable, delay);
            queue.offer(myTask);
            locker.notify();
        }
    }


    // 在这里构造工作线程, 负责执行具体任务
    public MyTimer() {
    
    
        Thread work = new Thread(() -> {
    
    
            while (true) {
    
    
                try {
    
    
                    synchronized (locker) {
    
    
                        // 队列为空阻塞等待
                        while (queue.isEmpty()) {
    
    
                            locker.wait();
                        }
                        MyTask myTask = queue.peek();
                        long curTime = System.currentTimeMillis();
                        if (curTime >= myTask.time) {
    
    
                            // 时间到了, 可以执行任务了
                            queue.poll();
                            myTask.runnable.run();
                        } else {
    
    
                            // 时间还没到,阻塞等待
                            locker.wait(myTask.time - curTime);
                        }
                    }
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        // 执行工作线程
        work.start();
    }
}

コードの説明:

  1. 上記ではプライオリティキューを使用しているため、PriorityQueueデータ要素 MyTask の比較メソッドを に追加する必要があります。
  2. コード内のwait/notify重要な機能は次のとおりです。
    (1) wait は、ビジー待機の問題を解決します。現在のキューの最も早いタスク時間がまだ到着していない場合、wait を使用しないと、while (true) の回転が速すぎて、無意味な CPU の浪費が発生します。たとえば、現在のキュー内の最も早いタスクは、1 分後に特定のロジックを実行するように設定されます。ただし、ここで while (true) を指定すると、チームの最初の要素が 1 秒あたり数万回アクセスされることになり、タスクが実行されるまでにはまだ長い時間がかかります。

    (2) Timer のスケジュール メソッドでは、新しく挿入されたタスクをすぐに実行する必要がある場合があるため、新しいタスクが到着するたびに、notify を使用してワーカー スレッドを起動します。たとえば、現在のキューの最も早いタスクの実行時刻が 10:30 で、現在時刻が 10:00 であるため、30 分間待機します。待機中に新しいタスクがキューに追加されて実行された場合、 10:10 になると、ウェイクアップ待機が必要になり、ワーカー スレッドは比較のためにキューの最初の要素を取得します。
  3. 上記のコードの同期ロックの位置は慎重に考慮されており、ランダムに追加することはできません。

PriorityBlockingQueue を使用しないのはなぜですか?

Java ライブラリにはブロッキング付きの優先キューも用意されています。このキュー自体はスレッドセーフですが、PriorityBlockingQueue を直接使用せず、PriorityQueue を使用して手動で待機を追加してみてはいかがでしょうか。実際、ブロガーもここで落とし穴に遭遇したことがあります。タイマーのブロッキング キュー バージョンを実装した後、タイマーには独自のブロッキングがあるため、タイマーを追加するとロックされるまで待機する必要があるため、デッドロックが発生しやすくなります。つまり、ブロッキング バージョンのタイマーは制御が容易ではありませんが、通常の優先キューを使用して手動で待機するよりも堅牢です。

4. スレッドプール

1. スレッドプールの概要

スレッドの作成はプロセスに比べて軽量ですが、頻繁に作成されるとオーバーヘッドが無視できなくなります。スレッド プールを使用すると、あらかじめ一定数のスレッドを用意して待機状態にしておくことで、スレッドの頻繁な作成と破棄によるオーバーヘッドを回避できます。タスクが到着すると、スレッド プール内のスレッドはすぐに応答してタスクを実行できるため、スレッドの作成と起動による遅延が回避されます。同時に、タスクの実行が完了した後、スレッドはすぐには破棄されず、スレッド プールに再結合され、次のタスクの到着を待ちます。したがって、スレッド プールにより、スレッドの作成と破棄が大幅に削減され、システムのパフォーマンスと安定性が向上します。

スレッド プールの最大の利点は、毎回のスレッドの開始と破棄による損失を軽減できることです。

スレッドを直接作成するよりも、スレッド プールからスレッドを取得する方が効率的なのはなぜですか?

スレッド プールからのスレッドの取得は、純粋にユーザー モードの操作です。システムからスレッドを作成するには、ユーザー モードとカーネル モードを切り替える必要があります。純粋なユーザーモード操作の場合、時間は制御可能ですが、カーネルモード操作になると、時間は制御しにくくなります。

既製のスレッド プールは Java 標準ライブラリで提供されますExecutorServiceたとえば、10 個のスレッドを含むスレッド プールを作成します。

ExecutorService pool = Executors.newFixedThreadPool(10);

これは直接新しい ExecutorService オブジェクトではありませんが、オブジェクトの構築は Executors クラスの静的メソッドを通じて完了します。ここでは実際に「ファクトリ パターン」が使用されており、Executors クラスはファクトリ クラスと同等であり、本質的には ThreadPoolExecutor クラスをカプセル化したものです。

2. ThreadPoolExecutor パラメータ

ThreadPoolExecutor は、スレッド プールの動作の設定をさらに調整できるオプションのパラメータをさらに提供します。公式の Java ドキュメントを参照し、java.util.concurrent パッケージで ThreadPoolExecutor の構築メソッドを見つけることができます。

以下では、構築メソッドにおけるこれらのパラメータの意味について詳しく説明します。

  1. corePoolSize - コアスレッドの数
  2. MaximumPoolSize - スレッドの最大数 (コア スレッドの数 + 一時スレッドの数)
  3. KeepAliveTime - 一時スレッドが存続する時間
  4. 単位 - 単位 s、ms、分
  5. workQueue - スレッド プールは、ブロッキング キューを通じて編成される多くのタスクを管理します。送信とは、タスクをキューに入れることです。
  6. threadFactory - ファクトリ モード、スレッドを作成するための補助クラス

: 現在のタスクが多数ある場合、システムはいくつかの一時スレッドを作成します。現在のタスクが比較的少なく、比較的アイドル状態である場合、スレッド プールは余分な一時ワーカー スレッドを破棄します。比較的アイドル状態の場合、一時スレッドはすぐには破棄されませんが、一定の存続期間があります。

上記の 6 つのパラメータに加えて、スレッド プールの拒否戦略を表す RejectedExecutionHandler 型のパラメータもありますので、以下で詳しく紹介します。拒否された実行ハンドラ

: スレッド プールの拒否戦略 (RejectedExecutionHandler) は、スレッド プール内のタスク キューがいっぱいで、すべてのスレッドがタスクを実行しているときに新しいタスク リクエストが到着したときに、このリクエストをどのように処理するかという戦略です。拒否戦略は、スレッドを拒否するのではなく、タスクを拒否することです (スレッド プールは、スレッド プールがいっぱいの場合はブロックしたがらず、空の場合のみブロックします)。

(1) AbortPolicy: オーバーロード、例外を直接スローする
(2) CallerRunsPolicy: 呼び出し元が処理を担当します。つまり、追加した人がそれを実行します
(3) DiscardOldestPolicy: キュー内の最も古いタスクを破棄します
(4) DiscardPolicy: 新しいタスクを破棄します

3. スレッドプールの実装をシミュレートする

public class MyThreadPool {
    
    
    // 管理任务的阻塞队列(本身就是多线程安全)
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue();
	
	// 添加任务方法
    public void submit(Runnable runnable) throws InterruptedException {
    
    
        queue.put(runnable);
    }

    // 实现一个固定线程个数的线程池
    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) {
    
    
                        e.printStackTrace();
                    }
                }
            });
            // 启动线程
            t.start();
        }
    }
}

テスト: 10 個のスレッドを構築し、1000 個のタスクを実行します。

public class TestMyThreadPool {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        MyThreadPool pool = new MyThreadPool(10);

        for (int i = 0; i < 1000; i++) {
    
    
            int number = i;
            pool.submit(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    System.out.println("pool"+number);
                }
            });

        }
    }
}

: 上記のテスト クラスでは、i の代わりに数値が出力されます。匿名内部クラスにはラムダのような変数キャプチャがあり、final または実際の Final しかキャプチャできないため、i を使用すると構文エラーが発生します (プロセス全体で変更はありません)。 。

4. スレッド プールを作成する 2 つの方法

  1. 合格遺言執行者ファクトリ クラスの作成は比較的簡単ですが、カスタマイズ機能は限られています。
  2. 合格スレッドプールエグゼキュータ作成、作成方法はより複雑ですが、カスタマイズ能力は強力です。

5. 拡張: 実際の開発においてスレッド プール内のスレッド数を決定するにはどうすればよいですか?

まず第一に、異なるプログラムとスレッドは異なるタスクを実行します。

  1. CPU を大量に使用するタスク (主にコンピューター作業を行う) は、CPU で実行する必要があります。
  2. IO 集中型のタスク、主に IO 操作の待機 (ハードディスクへの読み取りと書き込み、グラフィックス カードの読み取りと書き込みの待機など) は、多くの CPU を占有しません。

極端な場合:

  1. すべてのスレッドが CPU を使用する場合、スレッドの数は CPU コアの数 (論理コアの数) を超えてはなりません。
  2. すべてのスレッドが IO を使用する場合、スレッド数を多数に設定でき、カップ コアの数をはるかに超える可能性があります。

実際にはどのように設定されますか?

実際には、このような極端な状況はめったにありません。特定の状況はテストを通じて決定する必要があります。テストを通じて、最適な実行効率とリソース使用率を持つスレッドの数を決定できます。

おすすめ

転載: blog.csdn.net/LEE180501/article/details/130461903