スレッドプールの実用的なアプリケーション

スレッドプール設定

corePoolSize:コアスレッドの数

実行するタスクがない場合でも、コアスレッドは常に稼働します

スレッド数がコアスレッド数より少ない場合、アイドル状態のスレッドがあっても、スレッドプールは処理用の新しいスレッドの作成を優先します

allowCoreThreadTimeout = true(デフォルトはfalse)に設定すると、コアスレッドはタイムアウトによって閉じられます

(1)同時実行性が高く、タスク実行時間が短いサービスの場合、スレッドプール内のスレッド数をCPUコア数+ 1に設定して、スレッドコンテキストの切り替えを減らすことができます
。(2)同時実行性が低くタスク実行が長いサービスの場合時間については、次の点を区別する必要があり
ます。a)IO操作はCPUを占有しないため、ビジネス時間がIO操作に長時間集中している場合、つまりIOを集中的に使用するタスクの場合は、すべてのCPUをアイドル状態にしないでください。スレッドプール内のスレッド数を増やして、CPUに処理させることができます。より多くのビジネス
b)ビジネス時間がコンピューティング操作、つまりコンピューティングを多用するタスクに集中している場合、これを行う方法はありません。(1 )、スレッドプール内のスレッド数を少なく設定し、スレッドプール内のスレッド数を減らします。コンテキストスイッチング
(3)高い同時実行性と長いビジネス実行時間。このタイプのタスクを解決するための鍵は、スレッドプールではなく、アーキテクチャ全体の設計にあります。これらのビジネスの一部のデータをキャッシュできるかどうかを確認するための最初のステップです。サーバーは2番目のステップです。スレッドプールの設定については、を参照してください。 (2)。最後に、ミドルウェアを使用してタスクを分割および分離できるかどうかを確認するために、ビジネスの実行時間が長いという問題も分析する必要がある場合があります。

タスク:500〜1000と仮定した場合の1秒あたりのタスク数タスク
コスト:0.1秒と仮定した場合の各タスクに費やした時間応答時間:複数の計算を行うために1秒と仮定
した場合のシステムが許容できる最大応答時間corePoolSize=あたりに必要なスレッド数2番目の取引?スレッド数=タスク/(1 /タスクコスト)=タスク*タスクカウト=(500〜1000)* 0.1 = 50〜100スレッド。corePoolSize設定は50より大きくする必要があります。8020の法則によれば、1秒あたりのタスクの80%が800未満の場合、corePoolSizeを80に設定する必要があります。



queueCapacity:タスクキュー容量(ブロッキングキュー)

最も一般的に使用される2つのキュー
ArrayBlockingQueue

これは、配列構造に基づく制限付きブロッキングキューであり、基になる構造は配列です。

LinkedBlockingQueue

リンクリスト構造に基づく制限付きブロッキングキュー(サイズが設定されていない場合、デフォルトはInteger.MAX_VALUE)、基になる構造は単一リンクリストです

コアスレッドの数が最大になると、新しいタスクがキューに入れられて実行されます。長さが不当に設定されていると、マルチスレッドの力を発揮できません。キューのデフォルトの長さはintの最大値です。
キューの長さがこの長さに設定されている場合、スレッドプール
の数はcorePoolSizeまでしか増加しません。corePoolSizeの数が少なすぎると、マルチスレッドの力を発揮します。

Tomcatのスレッドプールキューの長さは無限ですが、スレッドプールはmaximumPoolSizeまで作成され、その後、要求は待機キューに配置されます。

Tomcatタスクキューorg.apache.tomcat.util.threads.TaskQueueは、LinkedBlockingQueueを継承し、offerメソッドをオーバーライドします。

    @Override
    public boolean offer(Runnable o) {
      //we can't do any checks
        if (parent==null) return super.offer(o);
        //we are maxed out on threads, simply queue the object
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
        //we have idle threads, just add it to the queue
        if (parent.getSubmittedCount()<(parent.getPoolSize())) return super.offer(o);
         //线程个数小于MaximumPoolSize会创建新的线程。
        //if we have less threads than maximum force creation of a new thread
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
        //if we reached here, we need to add it to the queue
        return super.offer(o);
    }

queueCapacity =(coreSizePool / taskcost)応答時間の
計算では、queueCapacity = 80 / 0.1
1=80を取得できます。これは、キュー内のスレッドが1秒待機できることを意味します。それを超えると、実行するために新しいスレッドを開く必要があり
ます。Integer.MAX_VALUEに設定できないため、キューが非常に大きくなり、スレッドはcorePoolSizeサイズのままになります。実行するスレッドを開くと、応答時間が大幅に増加します。

maxPoolSize:スレッドの最大数

スレッド数>=corePoolSizeで、タスクキューがいっぱいの場合。スレッドプールは、タスクを処理するための新しいスレッドを作成します

スレッド数=maxPoolSizeでタスクキューがいっぱいになると、スレッドプールはタスクの処理を拒否し、例外をスローします
maxPoolSize =(max(tasks)-queueCapacity)/(1 / taskcost)(maximum number of tasks-queue容量)/各スレッドの1秒あたりの処理能力=最大スレッド数
maxPoolSizeを取得するために計算=(1000-80)/ 10 = 92 rejectExecutionHandler
:特定の状況に応じて決定され、タスクは重要ではなく、破棄できます。タスクは重要です
。keepAliveTimeとallowCoreThreadTimeoutを処理するには、いくつかのバッファリングメカニズムを使用する必要があります。通常、デフォルトで十分です。

keepAliveTime:スレッドのアイドル時間

スレッドのアイドル時間がkeepAliveTimeに達すると、スレッドの数=corePoolSizeのデフォルトになるまでスレッドは終了します。

allowCoreThreadTimeout:コアスレッドがデフォルトでタイムアウトすることを許可します

allowCoreThreadTimeout = trueの場合、スレッド数=0になるまで待機します

rejectExecutionHandler:タスク拒否ハンドラー

特定の状況に応じて決定されます。タスクは重要ではなく、破棄できます。タスクが重要な場合は、いくつかのバッファーメカニズムを使用して処理する必要があります。

タスクが拒否される状況は2つあります。

1):スレッド数がmaxPoolSizeに達し、キューがいっぱいになると、新しいタスクは拒否されます。

2):スレッドプールがshutdown()を呼び出すと、キュー内のタスクの実行が終了するのを待ってからシャットダウンします。shutdown()の呼び出しとスレッドプールの実際のシャットダウンの間にタスクが送信された場合、新しいタスクは拒否されます。

スレッドプールはrejectedExecutionHandlerを呼び出して、このタスクを処理します。設定されていない場合、デフォルトはAbortPolicyであり、例外がスローされます

スレッドプールによって提供される拒否ポリシー:
ThreadPoolExecutor.AbortPolicy:タスクを中止し、実行時例外をスローします

public static class AbortPolicy implements RejectedExecutionHandler {
	public AbortPolicy() { }

	public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }

}

ThreadPoolExecutor.CallerRunsPolicy:実行のために呼び出し元のスレッドにタスクを割り当て、現在破棄されているタスクを実行します。これによって実際にタスクが破棄されるわけではありませんが、送信されたスレッドのパフォーマンスが大幅に低下する可能性があります。

public static class CallerRunsPolicy implements RejectedExecutionHandler {
	public CallerRunsPolicy() { }

	public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
}

ThreadPoolExecutor.DiscardPolicy:無視してください。何も起こりません

public static classDiscardPolicyはRejectedExecutionHandlerを実装します{ publicDiscardPolicy(){} public void rejectExecution(Runnable r、ThreadPoolExecutor e){ } }



ThreadPoolExecutor.DiscardOldestPolicy:キューから最初に(最後に実行された)キューに入ったタスクをキックします

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
	public DiscardOldestPolicy() { }
	
	public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
	
}

3つの戦略はすべて、元のタスクを破棄します。ただし、一部のビジネスシナリオでは、タスクを無礼に破棄することはできません。もう1つの拒否戦略は、スレッドプールのスレッドを開始して破棄されたタスクを処理することですが、問題は、スレッドプールがアイドル状態であっても、破棄されたタスクを実行せず、スレッドプールを呼び出すメインスレッドを待機することです。タスクを実行します。ミッションが終了するまで。
RejectedExecutionHandlerインターフェースを実装して、ハンドラーをカスタマイズします

スレッドプールの定義では、拒否ポリシーに次のような統一された実装インターフェイスがあることがわかります。

public interface RejectedExecutionHandler { void rejectExecution(Runnable r、ThreadPoolExecutor executor); }ビジネスニーズに応じて、ビジネスシナリオに合った処理戦略を定義できます。


1.Nettyのスレッドプール拒否ポリシー

 private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
        NewThreadRunsPolicy() {
            super();
        }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                final Thread t = new Thread(r, "Temporary task executor");
                t.start();
            } catch (Throwable e) {
                throw new RejectedExecutionException(
                        "Failed to start a new thread", e);
            }
        }
    }

Nettyの処理方法は、タスクを破棄しないことです。この考え方は、CallerRunsPolicyの利点に似ています。Nettyフレームワークのカスタム拒否戦略では、破棄されたタスクは新しいワーカースレッドを作成することで完了しますが、スレッドを作成するときに条件付きの制約がなく、リソースがある限り新しいスレッドを作成し続けることがわかります。処理のためのスレッドを許可します。

Dubboのスレッドプール拒否ポリシー

public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {

    protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class);

    private final String threadName;

    private final URL url;

    private static volatile long lastPrintTime = 0;

    private static Semaphore guard = new Semaphore(1);

    public AbortPolicyWithReport(String threadName, URL url) {
        this.threadName = threadName;
        this.url = url;
    }

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        String msg = String.format("Thread pool is EXHAUSTED!" +
                        " Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
                        " Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
                threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
                e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
                url.getProtocol(), url.getIp(), url.getPort());
        logger.warn(msg);
        dumpJStack();
        throw new RejectedExecutionException(msg);
    }

    private void dumpJStack() {
       //省略实现
    }
}

Dubboのカスタム拒否ポリシーでは、ログが出力され、現在のスレッドのスタック情報が出力され、JDKのデフォルトの拒否ポリシーが実行されます。

カスタマイズ

public class CustomRejectionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 打印日志、暂存任务、重新执行等拒绝策略
    }
}
 /**
     * 自定义拒绝策略
     */
    publice class CustomRejectedExecutionHandler implements RejectedExecutionHandler {

        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                // 核心改造点,由blockingqueue的offer改成put阻塞方法  
                executor.getQueue().put(r);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

タスクの量が多く、各タスクを正常に処理する必要がある場合は、送信されたタスクをブロックして送信し、拒否メカニズムを書き直して、代わりに送信をブロックする必要があります。タスクを放棄しないことが保証されています

ThreadPoolExecutorの実行順序

スレッドプールは、次の動作でタスクを実行します

(1)スレッド数がコアスレッド数より少ない場合は、スレッドを作成します。

(2)スレッド数がコアスレッド数以上で、タスクキューがいっぱいでない場合は、タスクをタスクキューに入れます。

(3)スレッド数がコアスレッド数以上で、タスクキューがいっぱいの場合

  a)若线程数小于最大线程数,创建线程

 b)若线程数等于最大线程数,抛出异常,拒绝任务

分析を使用する

ユーザーの要求に迅速に対応するビジネスシナリオでは、ユーザーエクスペリエンスを検討する必要があります。応答が速いほど良いです。ページを半日更新できない場合、ユーザーは製品の表示をあきらめる可能性があります。ユーザー指向の関数集約は通常、非常に複雑です。呼び出し間のカスケードおよびマルチレベルのカスケードにより、ビジネス開発の学生は、スレッドプールの単純な方法を使用して、呼び出しを並列タスクにカプセル化することを選択することがよくあります。実装により、全体的な応答時間が短縮されます。さらに、スレッドプールの使用も考慮されます。このシナリオで最も重要なことは、ユーザーを満足させるための最大応答速度を取得することです。したがって、並行タスクをバッファリングするキューを設定せず、corePoolSizeとmaxPoolSizeを増やして作成する必要があります。高速実行のためにできるだけ多くのスレッド。タスク。

バッチタスクを迅速に処理する場合、このシナリオでは多数のタスクを実行する必要があります。また、タスクの実行速度が速いほど良いことを願っています。この場合、並列コンピューティングにはマルチスレッド戦略も使用する必要があります。ただし、応答速度優先シナリオとの違いは、この種のシナリオは大量のタスクがあり、すぐに完了する必要がないことです。代わりに、限られたリソースを使用し、ユニットごとにできるだけ多くのタスクを処理する方法に焦点を当てています。時間の、つまりスループットの優先順位。問題。したがって、並行タスクをバッファリングするようにキューを設定し、適切なcorePoolSizeを調整して、タスクを処理するスレッドの数を設定する必要があります。ここで、設定するスレッドが多すぎると、スレッドコンテキストの切り替えが頻繁に発生し、処理タスクの速度が低下し、スループットが低下する可能性があります。

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.concurrent.*;
public class ThreadTask {
    public static void testHospTest() {
        // 创建一个名为shopTest-pool-%d的线程池
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("shopTest-pool-%d").build();
        /** 构建线程池参数
         *  1.corePoolSize 核心线程数量
         *  2.maximumPoolSize 能创建的最大线程数,最大线程数不能大于核心线程数
         *  3.keepAliveTime 也就是当线程空闲时,所允许保存的最大时间,超过这个时间,线程将被释放销毁,但只针对于非核心线程
         *  4.TimeUnit 时间单位,TimeUnit.MICROSECONDS等
         *  5.workQueue 工作队列,这里有几种
         *     5.1 ArrayBlockingQueue 基于数组的有界阻塞队列,必须设置容量,遵循先进先出原则(FIFO)对元素进行排序。
         *     5.2 LinkedBlockingQueue:一个基于链表结构的阻塞队列,可以设置容量,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue
         *     5.3 SynchronousQueue:一个不存储元素的阻塞队列。每个插入offer操作必须等到另一个线程调用移除poll操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。
         *     5.4 PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
         *  6.threadFactory 线程工厂,用于创建线程
         *  7.handler 当线程边达到最大容量时,用于处理阻塞时的程序策略
         *     7.1 ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
         *     7.2 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
         *     7.3 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
         *     7.4 ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
         *  8.executorService.execute 执行一个实现Runnable 接口的线程
         *  9.executorService.shutdown();停止线程池
         */
        // 构建线程参数
        ExecutorService executorService = new ThreadPoolExecutor(3,
                3, 0l,
                TimeUnit.MICROSECONDS,
                new LinkedBlockingQueue<>(3), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
        try {
            int a = 20;
            // 执行线程
            executorService.execute(new Seller(a));
        } catch (Exception e) {
            System.err.println("数值小于0"+e.getMessage());
        } finally {
            // 停止线程池
            executorService.shutdown();
        }


    }
}
public class Seller implements Runnable {

    private int ticket;

    public Seller(int ticket) {
        this.ticket = ticket;
    }

    @Override
    public void run() {
        if (ticket > 0) {
            while (ticket > 0) {
                ticket--;
                System.out.println("你已经白嫖了" + ticket + "次");
            }
        } else {
            System.err.println("输入参数有误");
        }
    }
}

実用的なアプリケーション

必要

医療にプッシュする必要のあるデータは、毎月最初の3日間で約3,000万個のデータですが、サードパーティの監督が提供するインターフェースでは、3,000個のデータプッシュしかサポートされていません。推定で3,000万個のデータがあります。データの場合、1つの3,000個のデータが3秒で計算されます。これには約25時間かかります。10,000個のデータをプッシュした後、データを検証し、失敗したデータを処理する必要があります。

したがって、複数のスレッドを導入して同時操作を実行し、データプッシュの時間を短縮し、データプッシュのリアルタイムパフォーマンスを向上させることが考えられます。

繰り返しを防ぐ

サードパーティにプッシュするデータは繰り返しプッシュしないでください。各スレッドによってプッシュされるデータを確実に分離するためのメカニズムが必要です。
データベースページングの方法を使用して、各スレッドは[start、limit]間隔でデータをプッシュし、開始の一貫性を確保する必要があります

故障メカニズム

また、スレッドがデータをプッシュできないことも考慮する必要があります。

独自のシステムの場合は、複数のスレッドによって呼び出されたメソッドを抽出し、トランザクション、スレッド例外、および全体的なロールバックを追加できます。

ただし、第三者との接続であり、取引はできませんので、データベースに直接障害状況を記録する方式を採用しており、後日、他の方法で障害データを処理することができます。

スレッドプールの選択

実際の使用では、スレッドプールを使用してスレッドを管理する必要があります。スレッドプールに関しては、ThreadPoolExecutorが提供するスレッドプールサービスを使用することがよくあります。SpringBootはスレッドプール非同期メソッドも提供しますが、SprignBoot非同期の方が便利な場合がありますが、ThreadPoolExecutorを使用します。スレッドプールを制御する方が直感的であるため、ThreadPoolExecutorコンストラクターを直接使用してスレッドプールを作成します。

コアコード


@Service
public class PushProcessServiceImpl implements PushProcessService {
    @Autowired
    private PushUtil pushUtil;
    @Autowired
    private PushProcessMapper pushProcessMapper;

    private final static Logger logger = LoggerFactory.getLogger(PushProcessServiceImpl.class);

    //每个线程每次查询的条数
    private static final Integer LIMIT = 300000;
    //起的线程数
    private static final Integer THREAD_NUM = 5;
    //创建线程池
    ThreadPoolExecutor pool = new ThreadPoolExecutor(THREAD_NUM, THREAD_NUM * 2, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));

    @Override
    public void pushData() throws ExecutionException, InterruptedException {
        //计数器,需要保证线程安全
        int count = 0;
        //未推送数据总数
        Integer total = pushProcessMapper.countPushRecordsByState(0);
        logger.info("未推送数据条数:{}", total);
        //计算需要多少轮
        int num = total / (LIMIT * THREAD_NUM) + 1;
        logger.info("要经过的轮数:{}", num);
        //统计总共推送成功的数据条数
        int totalSuccessCount = 0;
        for (int i = 0; i < num; i++) {
            //接收线程返回结果
            List<Future<Integer>> futureList = new ArrayList<>(32);
            //起THREAD_NUM个线程并行查询更新库,加锁
            for (int j = 0; j < THREAD_NUM; j++) {
                synchronized (PushProcessServiceImpl.class) {
                    int start = count * LIMIT;
                    count++;
                    //提交线程,用数据起始位置标识线程
                    Future<Integer> future = pool.submit(new PushDataTask(start, LIMIT, start));
                    //先不取值,防止阻塞,放进集合
                    futureList.add(future);
                }
            }
            //统计本轮推送成功数据
            for (Future f : futureList) {
                totalSuccessCount = totalSuccessCount + (int) f.get();
            }
        }
        //更新推送标志
        pushProcessMapper.updateAllState(1);
        logger.info("推送数据完成,需推送数据:{},推送成功:{}", total, totalSuccessCount);
    }

    /**
     * 推送数据线程类
     */
    class PushDataTask implements Callable<Integer> {
        int start;
        int limit;
        int threadNo;   //线程编号

        PushDataTask(int start, int limit, int threadNo) {
            this.start = start;
            this.limit = limit;
            this.threadNo = threadNo;
        }

        @Override
        public Integer call() throws Exception {
            int count = 0;
            //推送的数据
            List<PushProcess> pushProcessList = pushProcessMapper.findPushRecordsByStateLimit(0, start, limit);
            if (CollectionUtils.isEmpty(pushProcessList)) {
                return count;
            }
            logger.info("线程{}开始推送数据", threadNo);
            for (PushProcess process : pushProcessList) {
                boolean isSuccess = pushUtil.sendRecord(process);
                if (isSuccess) {   //推送成功
                    //更新推送标识
                    pushProcessMapper.updateFlagById(process.getId(), 1);
                    count++;
                } else {  //推送失败
                    pushProcessMapper.updateFlagById(process.getId(), 2);
                }
            }
            logger.info("线程{}推送成功{}条", threadNo, count);
            return count;
        }
    }
}

おすすめ

転載: blog.csdn.net/liuerchong/article/details/123866102