Javaスレッドプールは役に立たず、システムがクラッシュします。スレッドプールのパフォーマンスを最適化するにはどうすればよいですか。

コンテンツ

  • 背景紹介
  • スレッドプールのしくみ
  • スレッドプールの同時実行性の高いシナリオでの問題の分析
  • 同時実行性の高いシナリオでのスレッドプールのパフォーマンスの最適化
  • 要約する

背景紹介

みなさん、こんにちは。今日は、本番プロジェクトでのJavaスレッドプールの高度な同時実行性の最適化である比較的ハードコアなテクノロジーについて説明します。多くの兄弟は、Javaスレッドプールの理論的原理を聞いて、それがどのように機能するかを知っているかもしれません。しかし、私はプロジェクトでJavaスレッドプールを試したことがありません。ましてや、同時実行性の高い環境でのJavaスレッドプールの最適化は言うまでもありません。今日は、本番プロジェクトでのこのJavaスレッドプールの同時実行性の最適化について説明します。

スレッドプールのしくみ

スレッドプールについて話したいので、少なくともJavaスレッドプールの基本的な動作原理について少し知る必要があります。スレッドプールの原理を明確に説明したい場合、またはJDKスレッドプールのソースコードレベルを分析したい場合もあります。 、別の記事を書く必要があるかもしれません。これは今回のテーマではないので、この記事では、最初にスレッドプールの最も単純な原則について説明します。

スレッドプールは、簡単に言うと、通常は破棄されないスレッドのプールであり、常に存在します。その後、スレッドプールにタスクを送信し続けることができ、スレッドプールは実行のためにスレッドを取り出します。タスク。次の図1に示すように、タスクが実行された後、スレッドは終了せず、スレッドプールに戻り、待機を継続します。

図1

ただし、現時点では重要な問題があります。つまり、スレッドプール内のスレッドの数は通常制限されています。これは通常、Javaスレッドプールの実際の原則によるものであり、実際には、カスタマイズされた手段によって、 Javaスレッドプールにさまざまなパフォーマンスを持たせることができます。ここでは、最も基本的な状況について説明します。つまり、スレッドプール内のスレッドの数は固定され、制限されています。

したがって、一度にスレッドプールに送信するタスクが多すぎて、現時点ですべてのスレッドが独自のタスクの実行でビジー状態になっている場合、新しいタスクを送信する場合はどう思いますか?タスクを提出できますか?**下の図2に示すように:

図2

もちろん、提出することはできませんが、スレッドプールが現時点であなたを拒否することしかできないというのは本当ですか?**それは真実ではありません。この状況に対処するために、スレッドプールは通常、タスクを送信するためのキューを設定し、タスクを一定期間キューで待機させます。スレッドが現在のタスクの実行を終了した後そして再びアイドル状態になり、次にこのキュー内のタスクをプルして実行します。Javaスレッドプールは実際にはカスタマイズによって他のパフォーマンスを発揮できるため、これも通常の状況であることに注意してください。ただし、以下の図3に示すように、通常は次のようにスレッドプールを設定します。

画像3

スレッドプールの同時実行性の高いシナリオでの問題の分析

次に、質問があります。最も基本的なJavaスレッドプールの原理と使用法を上で紹介しましたが、実際に本番プロジェクトに投入した後、どのような問題が発生しますか?まず、最大の問題は、スレッドプールに送信されたタスクがすべてさまざまなネットワークIOタスクを実行する可能性があることです。たとえば、rpcが他のサービスを呼び出したり、バックグラウンドでデータベース内の大量のデータを処理したりするため、非常に以下の図4に示すように、スレッドがタスクを実行するのに数百ミリ秒から数秒、さらには数十秒の長い時間がかかる可能性があります。

図4

2番目の質問では、上の図がないことに誰もが気づきました。つまり、一部のタスクはrpc呼び出しであり、数百ミリ秒しかかからない場合があり、一部のタスクは大規模なデータ操作であり、数十秒かかる場合があります。したがって、実際には、共通のスレッドプールはさまざまなタスクを実行します。これは、タスクがrpc呼び出しであるか、大量のデータ処理である可能性があるため、スレッドプール内のスレッドがタスクを完了できるかどうかの不確実性につながります。

3番目の問題は、一部のタスクがhttpリクエストに含まれている可能性があることです。元々はhttpリクエストを処理している可能性があり、これにより複数の時間のかかるタスクが順番に処理されます。現在、パフォーマンスを最適化するには、複数のタスクが必要です。プールでは、複数のスレッドを使用して複数のタスクを同時に実行し、このリクエストのパフォーマンスを向上させます。このhttpリクエストは、これらの複数の同時実行タスクの実行が完了するのを待ってから、以下の図5に示すように、ユーザー:

図5

したがって、**究極の大きな問題**、本番プロジェクトで実行されているこのスレッドプールは、rpc呼び出しのタイミング、大規模なデータ処理のタイミング、フォアグラウンドhttpなど、共有するさまざまな異なるタスクに提供されます。したがって、複数のタスクが同時に要求されます。実稼働環境のビジー期間中に、次のシナリオが発生する可能性があります。スレッドプールが複数の**時限rpc呼び出しと時限大規模データ処理タスク**を実行しており、これらのタスクは特に時間がかかり、多くのスレッドがビジーで、いくつかのスレッドがアイドル状態になります。

このとき、システムがCエンドユーザーに提供するインターフェースには、同時アクセスのシナリオが多くあります。多数のHTTPリクエストが送信され、各リクエストをスレッドプールに送信して同時に実行する必要があるため、アイドル状態がいくつか発生します。以下の図6に示すように、高速で実行されているスレッドプール内のスレッドがいっぱいになると、多数のタスクがスレッドプールのキューに入り、順番に待機を開始します。

画像6

この場合、多くのhttpリクエストタスクがスレッドプールにキューイングされ、実行できず、httpリクエストが応答を返すことができないため、必然的に多数のhttpリクエストが停止し、ユーザーにクリック感を与えます。 APP / Webページのフロントエンドで、クリックしてクリックします。応答がありません。システムにフリーズの問題があります。以下の図7に示すように:

図7

同時実行性の高いシナリオでのスレッドプールのパフォーマンスの最適化

この本番環境の問題に対応するために必要な最初の最大の改善点は、さまざまなタスクをスレッドプールから分離して、相互に影響を与えないようにすることです。つまり、時限rpcタスクは1つのスレッドに配置します。プールし、大量のデータ処理タスクを別のスレッドプールの通常のDBに配置してから、HTTP要求マルチタスク同時処理を別のスレッドプールに配置します。全員が独自のスレッドプールとリソースを使用し、相互に影響を与えません。 。、以下の図8に示すように:

図8

如上图所做的话,我们有一个专门处理http请求的线程池,这压力一下子就下来了,因为http请求的任务通常耗时都在几十ms到一百ms级,整体速度很快,线程池里没有定时rpc和定时db访问这种耗时任务进来捣乱了,所以http请求的专有线程池可以轻松+愉快的快速处理所有http请求的任务,即使是在高并发场景下,可以通过线程池增加线程资源来合理抗下高并发压力。

另外就是对线上系统生产环境的线程池任务运行,我们通常会在公司里或者项目内研发统一的线程池监控框架,所有的线程池任务都需要封装到一个线程池监控框架提供的Class里,然后通过这个Class来实现任务的排队等待与运行耗时的两个维度的监控数据统计,如下面的代码所示。

// 线程任务包装类,用了**装饰设计模式 **

// 线程任务包装类,用了装饰设计模式
public class RunnableWrapper implements Runnable {
    
    // 实际要执行的线程任务
    private Runnable task;
    // 线程任务被创建出来的时间
    private long createTime;
    // 线程任务被线程池运行的开始时间
    private long startTime;
    // 线程任务被线程池运行的结束时间
    private long endTime;
    
    // 当这个任务被创建出来的时候,就会设置他的创建时间
    // 但是接下来有可能这个任务提交到线程池后,会进入线程池的队列排队
    public RunnableWrapper(Runnable task) {
        this.task = task;
        this.createTime = new Date().getTime();
    }
    
    // 当任务在线程池排队的时候,这个run方法是不会被运行的
    // 但是当任务结束了排队,得到线程池运行机会的时候,这个方法会被调用
    // 此时就可以设置线程任务的开始运行时间
    public void run() {
        this.startTime = new Date().getTime();
        
        // 此处可以通过调用监控系统的API,实现监控指标上报
        // 用线程任务的startTime-createTime,其实就是任务排队时间
        // monitor.report("threadName", "queueWaitTime", startTime-createTime);
        
        // 接着可以调用包装的实际任务的run方法
        task.run();
        
        // 任务运行完毕以后,会设置任务运行结束的时间
        this.endTIme = new Date().getTime();
        
        // 此处可以通过调用监控系统的API,实现监控指标上报
        // 用线程任务的endTime - startTime,其实就是任务运行时间
        // monitor.report("threadName", "taskRunTime", endTime - startTime);
    }
    
}
复制代码

大家通过上面的代码可以清晰的看到,只要我们所有提交到线程池的任务,都用一个框架统一封装的RunnableWrapper类,基于装饰模式来进行包装,此时就可以得到线程任务的创建时间、开始时间、结束时间,接着就可以计算出这个任务的排队耗时、运行耗时,通过监控系统进行上报。

此时我们通过在监控系统里配置告警条件,就可以实现不同线程池的每个任务的耗时指标上报,同时如果有某个线程池的某个线程排队耗时或者运行耗时超过了我们配置的阈值,就会自动告警,如下图9所示:

图9

总结

好了,今天这篇文章到这里为止,就给大家把我们的线程池在生产项目里的生产问题和高并发如何优化,以及生产环境下的监控方案,都告诉大家了,希望大家学以致用,以后在项目里用线程池的时候,能够灵活运用咱们文章里学到的知识点。

END

扫码 免费获取 600+页石杉老师原创精品文章汇总PDF

图片

原创技术文章汇总

图片

おすすめ

転載: juejin.im/post/7078105889751695397