並行プログラミング 6: タスクまたはスレッドのキャンセルとシャットダウン

目次

1. タスクをキャンセルする方法

1.1 - タスク割り込みメカニズム

1.2 - 中断ポリシー

1.3 - 割り込みへの応答

1.4 - フューチャーによるキャンセル

1.5 - 無停電ブロッキングの処理

2. スレッドベースのサービス(エグゼキュータ)を停止します。

2.1 - カウンターを使用して送信されたタスクの数を記録する

2.2 - 毒薬オブジェクトの使用

2.3 - 1 回だけ実行されるサービス

3. スレッドの異常終了を処理する

3.1 - キャッチされなかった例外の処理

4. JVM のシャットダウン

4.1 - クロージングフック

4.2 - デーモンスレッド

4.3 - ファイナライザー


        Java は、あるスレッドが別のスレッドの現在の作業を終了できる割り込み (割り込み) 連携メカニズムを提供します。

        この協調的なアプローチは必要ですが、タスク、スレッド、またはサービスを即時に停止することはほとんど望まれません。そのような即時停止は、共有データ構造が不整合な状態のままになるためです。代わりに、タスクとサービスを協調的な方法で記述することができます。つまり、タスクとサービスを停止する必要がある場合、まず現在実行中の作業をクリーンアップしてから終了します。これにより、キャンセル リクエストを発行するコードよりもタスク自体のコードの方がクリーンアップの実行方法をよく知っているため、柔軟性が向上します。//割り込み操作は、いつ割り込むかを割り込みスレッド自体によって決定する必要があります

1. タスクをキャンセルする方法

        Java にはスレッドを先制的に安全に停止する方法はなく、したがって、タスクを先制的に安全に停止する方法もありません。取り消しを要求するタスクとコードがネゴシエートされたプロトコルに従うという協調メカニズムのみが存在します。

        調整メカニズムの 1 つは 「キャンセル要求済み」 フラグを設定でき、タスクは定期的にフラグをチェックします。このフラグが設定されている場合、タスクは早期に終了します。

        次のプロシージャ PrimeGenerator は、キャンセルされるまで素数の列挙を続けます。cancel メソッドはキャンセルされたフラグを設定し、メイン ループは次の素数を検索する前に最初にこのフラグを確認します。このプロセスが確実に動作するには、キャンセルされたフラグのタイプが volatile である必要があります// フラグを介してイベントネゴシエーションを実行する

import static java.util.concurrent.TimeUnit.SECONDS;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 一种简单的取消策略
 * 素数生成器
 */
public class PrimeGenerator implements Runnable {

    private static ExecutorService exec = Executors.newCachedThreadPool();

    private final    List<BigInteger> primes = new ArrayList<BigInteger>();
    /**
     * 取消标志
     */
    private volatile boolean          cancelled;

    public void run() {
        BigInteger p = BigInteger.ONE;
        while (!cancelled) { //检测取消标志
            p = p.nextProbablePrime();
            synchronized (this) {
                primes.add(p);
            }
        }
    }

    public void cancel() {
        cancelled = true;
    }

    public synchronized List<BigInteger> get() {
        return new ArrayList<>(primes);
    }

    static List<BigInteger> aSecondOfPrimes() throws InterruptedException {
        PrimeGenerator generator = new PrimeGenerator();
        exec.execute(generator);
        try {
            SECONDS.sleep(1);
        } finally {
            //确保该代码必定执行
            generator.cancel();
        }
        List<BigInteger> bigIntegers = generator.get();
        bigIntegers.forEach(System.out::println);
        return bigIntegers;
    }

    public static void main(String[] args) throws InterruptedException {
        aSecondOfPrimes();
    }
}

        キャンセル可能なタスクには、キャンセル ポリシー ( Cancel Policy ) が必要です。キャンセル ポリシーでは、キャンセル操作の「方法」、「いつ」、「何を」が詳細に定義されます。つまり、他のコードがタスクのキャンセルをどのように要求するか、いつタスク キャンセルが要求されたかどうか、およびキャンセル要求に応じて何をすべきかを確認します//キャンセル方法、キャンセルの検出方法、キャンセルへの対応方法

        PrimeGenerator は単純なキャンセル戦略を使用します。クライアント コードは cancel を呼び出してキャンセルを要求します。PrimeGenerator は素数を検索する前にまずキャンセル リクエストがあるかどうかを確認し、あれば終了します。

1.1 - タスク割り込みメカニズム

        PrimeGenerator のキャンセル メカニズムにより、最終的には素数の検索タスクが終了しますが、終了プロセスには一定の時間がかかります。ただし、このメソッドを使用するタスクが BlockingQueue.put などのブロッキング メソッドを呼び出す場合は、より深刻な問題が発生する可能性があります。タスクはキャンセル フラグを確認しないため、タスクが終了しない可能性がありますコードは次のようになります: //ブロッキングメソッドがタスクの終了を妨げます -> そのため割り込みメカニズムが必要です

import java.math.BigInteger;
import java.util.concurrent.BlockingQueue;

class BrokenPrimeProducer extends Thread {

    private final    BlockingQueue<BigInteger> queue;
    private volatile boolean                   cancelled = false;

    BrokenPrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            while (!cancelled) {
                //阻塞队列:如果队列一直为满,可能出现无法取消的情况
                queue.put(p = p.nextProbablePrime());
            }
        } catch (InterruptedException consumed) {
        }
    }

    public void cancel() {
        cancelled = true;
    }
}

        Java の特別なブロッキング ライブラリの一部のメソッドは割り込みをサポートします。スレッドの中断は、スレッドが別のスレッドに通知して、適切または可能な場合に現在の作業を停止し、代わりに他の作業を実行するように指示できる協調メカニズムです。

        各スレッドにはブール値の割り込みステータスがあります。スレッドが中断されると、スレッドの中断ステータスが true に設定されます。Thread には、スレッドを中断し、スレッドの中断ステータスを問い合わせるためのメソッドが含まれています。interruptメソッドはターゲット スレッドに中断でき、isInterrupted メソッドはターゲット スレッドの中断ステータスを返すことができます。静的割り込みメソッドは、現在のスレッドの割り込みステータスをクリアし、以前の値を返します。これが、割り込みステータスをクリアする唯一の方法です。//Thread での割り込みの判定には、状態をクリアする方法とクリアしない方法の 2 通りがあります。

        Thread クラスの API ドキュメントはここにあります

Thread.sleepObject.waitなど        のブロッキング ライブラリ メソッドは、スレッドが中断されたときにチェックし、中断が検出された場合は早期に戻ります。割り込みに応じて実行される操作には、割り込みステータスのクリア、および割り込みによりブロック操作が途中で終了したことを示すInterruptedException のスローが含まれます。// InterruptedException は割り込みステータスをクリアします

        スレッドが非ブロッキング状態で割り込まれると、その割り込みステータスが設定され、次にその割り込みステータスがキャンセルされる操作に対してチェックされます。このようにして、割り込み操作は「スティッキー」になります。InterruptedException がトリガーされない場合、割り込み状態が明示的にクリアされるまで、割り込み状態は維持されます。

        割り込みの呼び出しは、ターゲット スレッドの進行中の作業を直ちに停止することを意味するのではなく、割り込みを要求するメッセージを渡すだけです。

        割り込み操作の正しい理解は、実行中のスレッドに実際に割り込むのではなく、割り込み要求を発行するだけで、次の適切な瞬間 (これらの瞬間はキャンセル ポイントとも呼ばれます) でスレッド自体が割り込みます。wait、sleepjoinなどの一部のメソッドは、この種のリクエストを厳密に処理し、割り込みリクエストを受信したり、実行開始時に設定された割り込みステータスを見つけたりすると、例外をスローします。

        BrokenPrimeProducer は、一部のカスタム キャンセル メカニズムがブロッキング ライブラリ関数とうまく連携しない理由を示しています。タスク コードが割り込みに応答できる場合、キャンセル メカニズムとして割り込みを使用し、多くのライブラリ クラスで提供される割り込みサポートを利用できます// キャンセルメカニズムとしては割り込みを優先する必要があります

        多くの場合、割り込みはキャンセルを実装する最も論理的な方法です

        したがって、BrokenPrimeProducer の問題は簡単に解決 (そして簡略化) できます。キャンセルを要求するにはブール値フラグの代わりに割り込みを使用します。プログラムは次のようになります。

    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            //使用中断机制来取消操作
            while (!Thread.currentThread().isInterrupted()) {
                queue.put(p = p.nextProbablePrime());
            }
        } catch (InterruptedException consumed) {
            /*允许线程退出*/
        }
    }

    //取消方法
    public void cancel() {
        interrupt();
    }

1.2 - 中断ポリシー

        割り込みポリシーは、スレッドが割り込み要求を解釈する方法、つまり割り込み要求が見つかったときにどのような作業を行うか、どの作業単位が割り込みのアトミック操作であるか、および割り込みにどのように迅速に応答するかを指定します。//割り込みへの応答方法

        割り込みに対するタスクとスレッドの反応を区別することが重要です。割り込みリクエストには 1 つ以上のレシーバーがあり、スレッド プール内の特定のワーカー スレッドに割り込むことは、「現在のタスクをキャンセルする」ことと「ワーカー スレッドを閉じる」ことを意味します。

        タスクは独自のスレッドではなく、サービスが所有するスレッドで実行されます。スレッド所有者ではないコードの場合は、「非所有者」コードが応答できる場合でも、スレッド所有者のコードが割り込みに応答できるように、割り込み状態を保存するように注意する必要があります。家の掃除をするとき、たとえ所有者が不在であっても、この間に受け取った雑誌は捨てるべきではなく、片付けて所有者が戻ってきたときに渡してください。ただし、雑誌を読むこともできます。) // Don他のスレッドの割り込みステータスを失わず (例外の後に割り込みフラグを再度追加する必要がある場合があります)、各スレッドは独自の割り込み処理ロジックを持つ必要があるため、このスレッド以外の割り込みに応答しません。

        そのため、ほとんどのブロッキング ライブラリ関数は、割り込みに応じて ImterruptedException をスローするだけです。これらは、自分が所有するスレッドで実行されることはありません。そのため、タスクまたはライブラリ コードに対して最も合理的なキャンセル ポリシーを実装します。つまり、できるだけ早く実行フローを終了し、呼び出し元に割り込み情報を渡します。これにより、コール スタックの上位コードが実行できるようになります。さらなる措置を講じてください// 割り込み例外をスローし、呼び出し元に処理します。

        タスクが割り込み要求を検出した場合、タスクはすべての操作を放棄する必要はなく、割り込み要求の処理を延期し、応答する適切な瞬間まで待つことができます。したがって、タスクは割り込み要求を記憶し、現在のタスクの完了後に InterruptedException をスローするか、割り込み要求が受信されたことを示す必要があります。この手法により、更新中に中断が発生した場合でもデータ構造が破損しないことが保証されます。//割り込み要求の処理を遅らせることの利点

        タスクが割り込みをキャンセルとして扱うか、または他の割り込み応答操作として扱うかに関係なく、実行中のスレッドの割り込み状態を保持するように注意する必要があります。InterruptedException を呼び出し元に渡す以外に他の操作を実行する必要がある場合は、InterruptedException をキャッチした後に中断された状態を復元する必要があります// 中断された例外をスローすると、中断されたフラグがクリアされます。

Thread.currentThread().interrupt();

        各スレッドには独自の割り込みポリシーがあるため、そのスレッドにとって割り込みが何を意味するかを理解していない限り、スレッドを割り込まないでください。

       //要するに、スレッド自体だけが割り込み要求に応答する方法を知っています。スレッドの割り込み処理ロジックがわからない場合は、スレッドを中断したり、スレッドの割り込みに応答したりしないでください。 (ただし、例外をキャッチした後は、その割り込みフラグを復元します)。

1.3 - 割り込みへの応答

        Thread.sleep や BlockingQueue.put などの割り込み可能なブロック関数を呼び出す場合、InterruptedException を処理するには 2 つの実用的な方法があります。

  1. (おそらくタスク固有のクリーンアップ操作を実行した後)例外を渡すと、メソッドが割り込み可能なブロッキング メソッドにもなります。
  2. 呼び出しスタックの上位のコードが割り込みステータスを処理できるように、割り込みステータスを復元します。

        InterruptedException を渡したくない場合は、割り込み要求を保存する方法が必要です。標準的な方法は、再度中断を呼び出して中断された状態を復元することですスレッドの割り込み戦略がコードに実装されていない限り、何もせずに catch ブロックで例外をキャッチするなど、InterruptedException を検査することはできません//要約すると、この例外の処理方法がわからない場合は例外を渡し、割り込み戦略がわかっている場合はそれを処理します (割り込み状態を復元することも戦略です)。ただし、それを飲み込むことはできませんそしてそれを処理します。これがすべての例外の処理方法です。

        スレッド割り込みポリシーを実装するコードのみが割り込み要求をマスクできます。通常のタスクコードもライブラリコードも割り込み要求をマスクすべきではありません。

        //プログラミングするときは、スレッドが割り込みマークをクリアして正しく応答せず、無限ループが発生することを避けるために、割り込みマークのクリアポイントに必ず注意してください。さらに多くのプログラムをデバッグしますが、そうはならないと思います

        単一タスクのスレッド中断の例:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class InterruptedTest {

    private static ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            try {
                //模拟任务执行时间消耗
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                System.out.println("线程中断执行!任务结束");
                e.printStackTrace();
                return;
            }
            System.out.println("执行任务结束!");
        });
        threadA.start();
        // 使用一个定时任务来中断threadA任务的执行:2s中后中断线程
        scheduledExecutor.schedule(() -> threadA.interrupt(),
            2000, TimeUnit.MILLISECONDS);
        // 使用join等待threadA结束
        threadA.join();
        System.out.println("主线程运行结束");
    }
}

1.4 - フューチャーによるキャンセル

ExecutorService.submit は、タスクを説明するFuture        を返します。Future にはcancelメソッドがあり、これにはブール型パラメーターMayInterruptlfRunningがあり、タスクが実行中の場合に中断する必要があるかどうかを示します。 false の場合、実行中のタスクは完了できます。

        Future によるタスクキャンセルの実装例: //終了したスレッドを中断するには、interrupt() メソッドを使用しても問題ありませんが、スレッドが実行されているかどうかを考慮せずに cancel メソッドを呼び出せるという利点があります。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class InterruptedTest {

    private static ExecutorService executorService = Executors.newFixedThreadPool(2);

    public static void main(String[] args) throws InterruptedException {
        //1-定义一个带有返回值的任务
        Callable<String> callable = () -> {
            System.out.println("Callable 任务开始执行...");
            Thread.sleep(3000); //模拟执行时间
            System.out.println("Callable 任务执行结束...");
            return "end";
        };
        //2-提交任务
        Future<String> submit = executorService.submit(callable);

        try {
            try {
                //3-获取任务结果
                submit.get(2000, TimeUnit.MILLISECONDS);
            } catch (TimeoutException e) {
                System.out.println("TimeoutException->获取线程返回值超时...");
                e.printStackTrace();
            }
        } catch (ExecutionException e) {
            System.out.println("ExecutionException->任务执行异常");
            e.printStackTrace();
        } finally {
            //如果此时任务已经执行完成,执行取消操作也没有任何的影响
            System.out.println("尝试取消任务的执行");
            //4-取消任务执行-> 参数为false,还是会等待线程任务结束/为true,则直接中断任务
            submit.cancel(false);
        }
        System.out.println("主线程运行结束");
    }
}

        上の例のように、結果が必要なくなったタスクをキャンセルすることは、良いプログラミング方法です。

1.5 - 無停電ブロッキングの処理

        Java ライブラリでは、多くのブロッキング メソッドが早期に返すか InterruptedException をスローすることで割り込み要求に応答するため、開発者はキャンセル要求に応答するタスクを簡単に構築できます。ただし、すべてのブロッキング メソッドまたはブロッキング メカニズムが割り込みに応答できるわけではありません//次の例では、通常、シャットダウン メカニズムを使用して割り込みを行います。

同期ソケット I/O を実行するか、組み込みロックの取得を待機すること        によってスレッドがブロックされた場合、割り込み要求はスレッドの割り込みステータスを設定するだけで、他の効果はありません。中断不可能な操作によりブロックされたスレッドについては、中断と同様の手段を使用してこれらのスレッドを停止できますが、そのためにはスレッドがブロックされた理由を知る必要があります。

        Java.IO パッケージの同期ソケット I/Oサーバー アプリケーションでは、I/O をブロックする最も一般的な形式は、ソケットへの読み取りと書き込みです。InputStream や OutputStream の read や write などのメソッドは割り込みに応答しませんが、基になるソケットを閉じることで、read や write などのメソッドの実行によりブロックされたスレッドが SocketException をスローする可能性があります。

        Java.IO パッケージの同期 I/OInterruptibleChannel で待機しているスレッドを中断すると、ClosedByInterruptExceptionがスローされ、リンクが閉じられます (これにより、リンク上でブロックされている他のスレッドも ClosedByInterruptException をスローします)。InterruptibleChannel を閉じると、リンク操作でブロックされたすべてのスレッドがAsynchronousCloseExceptionをスローします最も標準的なチャネルはInterruptibleChannelを実装します。

        セレクターの非同期 I/OSelector.select メソッド (java.niochannels 内) の呼び出し中にスレッドがブロックした場合、close メソッドまたは wakeup メソッドを呼び出すと、スレッドは ClosedSelectorException をスローして早期に戻ります。

        ロックを取得しますスレッドが組み込みロックの待機中にブロックされている場合、スレッドは必ずロックを取得すると信じているため、割り込みに応答できなくなり、割り込み要求を無視します。ただし、lockInterruptibly メソッドが Lock クラスで提供されているため、割り込みに応答しながらロックを待機することができます。//Lock ロックの柔軟性を反映します

public class ReaderThread extends Thread {

    private static final int         BUFSZ = 512;
    private final        Socket      socket;
    private final        InputStream in;

    public ReaderThread(Socket socket) throws IOException {
        this.socket = socket;
        this.in     = socket.getInputStream();
    }
    //封装非标准的取消操作
    public void interrupt() {
        try {
            socket.close(); //关闭socket
        } catch (IOException ignored) {
        } finally {
            super.interrupt();
        }
    }

    public void run() {
        try {
            byte[] buf = new byte[BUFSZ];
            while (true) {
                int count = in.read(buf);
                if (count < 0) {
                    break;
                } else if (count > 0) {
                    processBuffer(buf, count);
                }
            }
        } catch (IOException e) { /* Allow thread to exit */
        }
    }

    public void processBuffer(byte[] buf, int count) {
    }
}

上記のコードは、非標準のキャンセル操作をラップする        方法を示していますReaderThread はソケット接続を管理し、ソケットから同期的にデータを読み取り、受信したデータを processBuffer に渡します。ユーザーの接続を終了するか、サーバーを閉じるために、ReaderThread は標準の割り込みを処理し、基礎となるソケットを閉じることができるように割り込みメソッドを書き換えますしたがって、ReaderThread スレッドが読み取りメソッドでブロックされているか、割り込み可能なブロック メソッドでブロックされているかに関係なく、中断され、現在の作業の実行が停止される可能性があります。//割り込みに応答できないブロック方式については、割り込みが発生しているように見える方法を提供し、より総合的に考慮することで、プログラムがブロックによって割り込み要求への応答が遅れることはありません

2. スレッドベースのサービス(エグゼキュータ)を停止します。

        サービス (エグゼキュータ) は、サービス自体とそのサービスが所有するスレッドをシャットダウンするためのライフサイクル メソッドを提供する必要があります。このようにして、アプリケーションがサービス (実行者) を閉じると、サービス (実行者) はすべてのスレッドを閉じることができます。shutdown や shutdownNow などのメソッドはExecutorServiceで提供されます 同様に、スレッドを持つ他のサービスでも同様のシャットダウン メカニズムを提供する必要があります。// Service はスレッド プールの保持オブジェクトである ExecutorService を参照します

2.1 - カウンターを使用して送信されたタスクの数を記録する

        サービスが閉じられた場合、一方では新しいタスクの送信を禁止する必要がありますが、他方では、サービスにはまだ未完了のタスクがあるため、サービスを完全に閉じる前にすべてのタスクが完了するまで待機する必要があります。

        ログはほとんどのサーバー アプリケーションで使用されます。たとえば、コードに println ステートメントを挿入すると、単純なログになります。PrintWriter のような文字ストリーム クラスはスレッドセーフであるため、この単純なアプローチでは明示的な同期は必要ありません。ただし、このインライン ログ機能は、一部の大容量 (Highvolume) アプリケーションに一定のパフォーマンス オーバーヘッドをもたらしますもう 1 つの方法は、log メソッドを呼び出して、他のスレッドによる処理のためにログ メッセージをキューに置くことです//インラインロギングは非効率的であり、プログラムのパフォーマンスに影響します

        以下は、ログを出力するための新しいスレッドを開始し、同時に LogService ログ サービスを安全に閉じることができるログ実装プログラムです。

public class LogService {
    //任务队列
    private final BlockingQueue<String> queue;
    private final LoggerThread          loggerThread;
    private final PrintWriter           writer;
    //关闭标志:因为该变量一直在锁中操作,所以不需要加 volatile
    private       boolean               isShutdown;
    //提交任务计数
    private       int                   reservations;

    public LogService(Writer writer) {
        this.queue        = new LinkedBlockingQueue<>();
        this.loggerThread = new LoggerThread();
        this.writer       = new PrintWriter(writer);
    }

    public void start() {
        loggerThread.start();
    }

    public void stop() {
        synchronized (this) {
            isShutdown = true;
        }
        loggerThread.interrupt();
    }

    public void log(String msg) throws InterruptedException {
        synchronized (this) {
            if (isShutdown) {
                throw new IllegalStateException(/*...*/);
            }
            ++reservations;
        }
        queue.put(msg);
    }

    private class LoggerThread extends Thread {

        public void run() {
            try {
                while (true) {
                    try {
                        synchronized (LogService.this) {
                            if (isShutdown && reservations == 0) {
                                break;
                            }
                        }
                        String msg = queue.take();
                        synchronized (LogService.this) {
                            --reservations;
                        }
                        writer.println(msg);
                    } catch (InterruptedException e) { /* retry */
                    }
                }
            } finally {
                writer.close();
            }
        }
    }
}

        ExecutorService は、 shutdown を使用した通常のシャットダウンとshutdownNowを使用した強制シャットダウンの 2 つのシャットダウン方法を提供します強制シャットダウンを実行する場合、 shutdownNow はまず現在実行中のタスクを閉じてから、すべての未開始タスクのリストを返します。

        これら 2 つのシャットダウン方法の違いは、それぞれの安全性応答性にあります。強制シャットダウンは高速ですが、タスクが実行途中で終了する可能性が高いため、リスクも高くなります。一方、通常のシャットダウンは時間がかかりますが、ExecutorService はキュー内のすべてのタスクが実行されるまで待機してから閉じるため、より安全です。スレッドを持つ他のサービスでも、同様のシャットダウン方法を選択できるようにすることを検討する必要があります。//閉じるには 2 つの方法があります。1 つは高速応答ですが安全ではありません。もう 1 つは安全ですが高速応答ではありません。

        したがって、上記のログサービスの stop メソッドは次のように書くこともできます。//注、スレッドを中断しないでください。そうしないと、すべてのタスクが完了するのを待たず、待機中のプロセスが中断される可能性があります。

    //使用线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();

    public void stop() {
        try {
            //关闭executorService,等待所有任务执行完成
            executorService.shutdown();
            //阻塞线程,直到所有任务完成
            executorService.awaitTermination(Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writer.close();
        }
    }

2.2 - 毒薬オブジェクトの使用

プロデューサー/コンシューマーサービス        を閉じるもう 1 つの方法は、 「ポイズン ピル (Poison Pil)」オブジェクトを使用することです。「ポイズン ピル」とは、キューに配置されたオブジェクトを指します。これは、「このオブジェクトを取得したら、すぐに停止する」ことを意味します。 FIFO (先入れ先出し) キューでは、「ポイズン ピル」オブジェクトは、コンシューマーが閉じる前にキュー内のすべてのジョブを最初に完了することを保証し、「ポイズン ピル」オブジェクトが処理される前に送信されたすべてのジョブを保証します。プロデューサーは、「毒薬」オブジェクトを送信した後は、それ以上の作業を送信できません。//ポイズンピルも割り込みマーカーの一種です

        サンプルプログラム:

public class IndexingService {

    private static final int                 CAPACITY = 1000;
    //毒丸对象
    private static final File                POISON   = new File("");
    private final        IndexerThread       consumer = new IndexerThread();
    private final        CrawlerThread       producer = new CrawlerThread();
    private final        BlockingQueue<File> queue;
    private final        FileFilter          fileFilter;
    private final        File                root;

    public IndexingService(File root, final FileFilter fileFilter) {
        this.root       = root;
        this.queue      = new LinkedBlockingQueue<>(CAPACITY);
        this.fileFilter = new FileFilter() {
            public boolean accept(File f) {
                return f.isDirectory() || fileFilter.accept(f);
            }
        };
    }

    private boolean alreadyIndexed(File f) {
        return false;
    }

    class CrawlerThread extends Thread {

        public void run() {
            try {
                crawl(root);
            } catch (InterruptedException e) { /* fall through */
            } finally {
                while (true) {
                    try {
                        queue.put(POISON);
                        break;
                    } catch (InterruptedException e1) { /* retry */
                    }
                }
            }
        }

        private void crawl(File root) throws InterruptedException {
            File[] entries = root.listFiles(fileFilter);
            if (entries != null) {
                for (File entry : entries) {
                    if (entry.isDirectory()) {
                        crawl(entry);
                    } else if (!alreadyIndexed(entry)) {
                        queue.put(entry);
                    }
                }
            }
        }
    }

    class IndexerThread extends Thread {

        public void run() {
            try {
                while (true) {
                    File file = queue.take();
                    if (file == POISON) {
                        break;
                    } else {
                        indexFile(file);
                    }
                }
            } catch (InterruptedException consumed) {
            }
        }

        public void indexFile(File file) {
            /*...*/
        }

        ;
    }

    public void start() {
        producer.start();
        consumer.start();
    }

    public void stop() {
        producer.interrupt();
    }

    public void awaitTermination() throws InterruptedException {
        consumer.join();
    }
}

        Poison Pill オブジェクトは、プロデューサーとコンシューマーの数がわかっている場合にのみ使用できます。このアプローチは、生産者と消費者の数が多い場合には使用が困難になります。Poison Pill オブジェクトは、無制限のキューでのみ確実に動作します//消費者が 5 人の場合、毒薬オブジェクトを 5 つ置き、5 人の消費者を中断します

2.3 - 1 回だけ実行されるサービス

        メソッドがタスクのバッチを処理し、すべてのタスクが処理されたときに戻る必要がある場合は、プライベート Executor を使用してサービスのライフ サイクル管理を簡素化できます。Executor のライフ サイクルはこのメソッドによって制御されます。この場合、通常、 invokeAllinvokeAnyなどのメソッドがより大きな役割を果たします。

3. スレッドの異常終了を処理する

        どのコードでも RuntimeException をスローする可能性があります。別のメソッドを呼び出すときは常に、その動作に懐疑的であり、メソッドが正常に返る必要がある、またはメソッド プロトタイプで宣言されたチェック例外の 1 つをスローする必要があるなどと盲目的に想定しないでください。呼び出し元のコードに詳しくなければ、その動作についてより懐疑的になる必要があります。

スレッドの欠落        による重大な結果の例: タイマーによって表されるサービスは利用できなくなります。// 時限タスクを実行しているスレッドが例外をスローした場合、スレッドは終了し、他のタスクは実行されません

        したがって、Runnable などの抽象メカニズムを通じて不明で信頼できないコードを呼び出す場合。RuntimeException のキャッチを考慮する必要があるため、スレッドはtry-catchコード ブロックでこれらのタスクを呼び出す必要があります。//例外をキャッチしてスレッドの省略/終了を防止します

        スレッドミス:これは、タスクを実行するためにスレッドがプールから削除され、タスクの完了後にスレッドがプールに戻されなかった場合に発生します。

3.1 - キャッチされなかった例外の処理

        未チェックの例外を解決するためにプロアクティブな方法を使用することに加えて。UncaughtExceptionHandlerも Thread API で提供されており、キャッチされなかった例外によってスレッドが終了したことを検出できます。これら 2 つのアプローチは補完的であり、それらを組み合わせることで、糸漏れの問題を効果的に防止できます。//アクティブな検査 + キャプチャの失敗

        例外ハンドラーがキャッチされなかった例外をどのように処理するかは、サービスの品質要件によって異なります。最も一般的な対応は、エラー メッセージと対応するスタック トレースをアプリケーション ログに書き込むことです例外ハンドラーは、スレッドの再起動、アプリケーションのシャットダウン、またはその他の修復や診断の実行など、より即時的な応答を行うこともできます。

import java.util.logging.Level;
import java.util.logging.Logger;

public class UEHLogger implements Thread.UncaughtExceptionHandler {

    public void uncaughtException(Thread t, Throwable e) {
        Logger logger = Logger.getAnonymousLogger();
        logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e);
    }
}

        長時間実行されるアプリケーションでは、すべてのスレッドからのキャッチされなかった例外に対して同じ例外ハンドラーを指定するのが一般的で、そのハンドラーは少なくとも例外情報を記録します。

4. JVM のシャットダウン

        JVM は正常にシャットダウンすることも、強制的にシャットダウンすることもできます。正常なシャットダウンは、最後の「通常 (非デーモン)」スレッドが終了したとき、またはSystem . 強制的にシャットダウン : Runtime.haltを呼び出すか、オペレーティング システムでJVM プロセスを強制終了」する (たとえば、SIGKILL を送信する) ことによって、JVM を強制的にシャットダウンできます。

4.1 - クロージングフック

        通常のシャットダウンでは、JVM は最初に登録されているすべてのシャットダウン フック (シャットダウン フック) を呼び出します。シャットダウン フックは、 Runtime.addShutdownHookを通じて登録されているがまだ開始されていないスレッドを指しますJVM はシャットダウン フックが呼び出される順序を保証しません。アプリケーション スレッドをシャットダウンするときに、まだ実行中のスレッドがある場合、それらのスレッドはシャットダウン プロセスと同時に実行されます。すべてのシャットダウン フックが実行されるときに、runFinalizersOnExitが true の場合、JVM は停止する前にファイナライザーを実行します。JVM は、シャットダウン時にまだ実行されていたアプリケーション スレッドを停止したり中断したりしません

4.2 - デーモンスレッド

        補助的な作業を実行するスレッドを作成したいが、このスレッドが JVM のシャットダウンをブロックしたくない場合。この場合、デーモン スレッド( Daemon Thread ) を使用する必要があります。//典型的なのはガベージコレクターです

        スレッドは、通常のスレッドとデーモン スレッドの 2 種類に分類できます。JVM の起動時に作成されるすべてのスレッドのうち、メイン スレッド以外のすべてのスレッドはデーモン スレッド (ガベージ コレクターや補助的な作業を実行するその他のスレッドなど) です。デフォルトでは、メインスレッドによって作成されるすべてのスレッドは通常のスレッドです

        通常のスレッドとデーモン スレッドの違いは、スレッドが終了するときに何が起こるかだけですスレッドが終了すると、JVM は他の実行中のスレッドを確認し、それらのスレッドがデーモン スレッドの場合、JVM は通常の終了操作を実行します。JVM が停止すると、残っているデーモン スレッドはすべて破棄され、最終ブロックは実行されず、スタックも巻き戻されません。//デーモンスレッドと通常スレッドの動作は終了時の扱いのみ

デーモン スレッドの使用はできる限り少なくする必要        があります。クリーンアップなしで安全に破棄できる操作はほとんどありません特に、デーモン スレッドで I/O 操作を伴う可能性のあるタスクを実行するのは危険です。デーモン スレッドは、メモリ内キャッシュから期限切れのデータを定期的に削除するなど、「内部」タスクを実行するのに最適です。

4.3 - ファイナライザー

        メモリ リソースは、不要になったときにガベージ コレクターによって再利用できますが、ファイル ハンドルやソケット ハンドルなどの他のリソースは、不要になったときに明示的にオペレーティング システムに返す必要があります。この機能を実現するために、ガベージ コレクターは、finalize メソッドを定義するオブジェクトに対して特別な処理を実行します。コレクターがオブジェクトを解放した後、finalizeメソッドを呼び出して、一部の永続リソースが確実に解放されるようにします。

        ファイナライザーは JVM によって管理されるスレッドで実行できるため、ファイナライザーによってアクセスされる状態は複数のスレッドによってアクセスされる可能性があるため、そのアクセスは同期する必要があります。ファイナライザは、いつ実行されるか、実行されるかどうかさえ保証されません。また、複雑なファイナライザは、オブジェクトに多大なパフォーマンスのオーバーヘッドを引き起こすことがよくあります正しいファイナライザーを書くのは非常に困難です。

        ほとんどの場合、ファイナライザーを使用するよりも、finally ブロックと明示的な close メソッドを使用する方が、リソースをより適切に管理できます。唯一の例外は、オブジェクトを管理する必要があり、オブジェクトが保持するリソースがネイティブ メソッドを通じて取得される場合ですこれらの理由およびその他の理由により、ファイナライザーを含むクラス (プラットフォーム ライブラリを除く) の作成または使用を避けるよう努めています。//ファイナライザーはネイティブ メソッドを呼び出すクラスにのみ使用する必要があります

        ファイナライザーの使用を避ける

        今回で全文はここで終わります。

おすすめ

転載: blog.csdn.net/swadian2008/article/details/119601492