スレッドプール実行の原則

目次

スレッドプールの基本的な紹介

スレッドプールパラメータ

スレッドプールのステータスと基本的なメソッド

スレッドプールの実行プロセス

組み込みのスレッドプール

newFixedThreadPool

newSingleThreadExecutor

newCachedThreadPool

newScheduledThreadPool


スレッドプールの基本的な紹介

プロジェクト内のスレッドプールの出現率はまだ比較的高いです。スレッドプールがより一般的な言語で導入されている場合、それはスレッドコレクション+タスクキューです。タスク要求がスレッドプールに到着すると、それに詰め込まれます。タスクキュー、そしてスレッドがエンドレスループでキューからタスクを取得する単純なシナリオ。では、なぜスレッドプールが必要なのですか?

  • スレッドの頻繁な作成と破棄を回避し、リソース消費を削減します
  • スレッドプールは、割り当て、調整、監視などのスレッドリソースをより適切に管理できます
  • 応答速度を向上させます。タスクが来ると、スレッドが作成されるのを待たずに応答できます。

スレッドプールパラメータ

実際、jdkにはいくつかの組み込みスレッドプールが用意されていますが、それらを使用することはお勧めしません。手動で作成することをお勧めします。スレッドプールを作成するためのメソッドパラメータは次のとおりです。

public ThreadPoolExecutor(int corePoolSize, // 核心线程数量
                              int maximumPoolSize, // 线程池最大线程数量
                              long keepAliveTime, // 非核心线程数存活时间
                              TimeUnit unit, // 存活时间单位
                              BlockingQueue<Runnable> workQueue, // 队列,存放任务
                              ThreadFactory threadFactory, // 线程工厂,一般用于取名
                              RejectedExecutionHandler handler) { // 线程拒绝策略
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

以下では、これらのパラメーターについて少し詳しく説明し、スレッドプールの実行プロセスのソースコードと組み合わせて各パラメーターの使用法を分析します。

int corePoolSize:线程池中的核心线程数量,即使没有任务,这些线程也不会销毁。
int maximumPoolSize:线程池中的最大线程数量,即线程池所支持创建的最大数量线程,即使任务超级多,也只会有 maximumPoolSize 数量的线程在运行。
long keepAliveTime:非核心线程的存活时间,当任务数量超过核心线程数量时,只要 corePoolSize < maximumPoolSize,线程池便会创建对应的线程数去执行任务,当线程池中存活的线程数量大于核心线程时,如果等了 keepAliveTime 时间仍然没有任务进来,则线程池会回收这些线程。
TimeUnit unit:非核心线程存活时间的具体单位,即等待多少毫秒、秒等。
BlockingQueue<Runnable> workQueue:存储线程任务所用的队列,提交的任务将会被放到该队列中。
ThreadFactory threadFactory:线程工厂,主要用来创建线程的时候给线程取名用,默认是pool-1-thread-3
RejectedExecutionHandler handler:线程拒绝策略,当存储任务所用的队列都被填满时,新来的任务此时无处存放,那么需要提供一种策略去解决这种情况。

スレッドプールのステータスと基本的なメソッド

このクラスは、いくつかのスレッドプールの基本的な状態と基本的なメソッドを定義します。ソースコードを読む前に、それについて知っておく必要があります。

/*
 * 该对象高3位维护线程池运行状态,低29位维护当前线程池数量
 */
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
/*
 * 该值等于29,用于左移
 */
private static final int COUNT_BITS = Integer.SIZE - 3;
/*
 * 线程池支持的最大线程数量,即29位所能表示的最大数值,2^29 - 1
 */
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
/*
 * 即高3位为111,低29位为0,该状态的线程池会接收新任务,并且处理阻塞队列中正在等待的任务
 */
private static final int RUNNING    = -1 << COUNT_BITS;
/*
 * 即高3位为000,不接受新任务,但仍会处理阻塞队列中正在等待的任务
 */
private static final int SHUTDOWN   =  0 << COUNT_BITS;
/*
 * 高3位为001,不接受新任务也不处理阻塞队列中的任务
 */
private static final int STOP       =  1 << COUNT_BITS;
/*
 * 高3位为010,所有任务都被终止了,workerCount为0,为此状态时还将调用terminated()方法
 */
private static final int TIDYING    =  2 << COUNT_BITS;
/*
 * 高3位为011,terminated()方法调用完成后变成此状态
 */
private static final int TERMINATED =  3 << COUNT_BITS;

これらの状態はint型で表され、サイズの関係はRUNNING <SHUTDOWN <STOP <TIDYING <TERMINATEDであり、このシーケンスは基本的に、実行から終了までのスレッドプールのプロセスに従います。このクラスでは、スレッドプールのステータスとスレッド数を取得するために、次の3つのメソッドが一般的に使用されます。

/*
 * c & 高3位为1,低29位为0的~CAPACITY,用于获取高3位保存的线程池状态
 */
private static int runStateOf(int c)     { return c & ~CAPACITY; }
/*
 * c & 高3位为0,低29位为1的CAPACITY,用于获取低29位的线程数量
 */
private static int workerCountOf(int c)  { return c & CAPACITY; }
/*
 * 参数rs表示runState,参数wc表示workerCount,即根据runState和workerCount打包合并成ctl
 */
private static int ctlOf(int rs, int wc) { return rs | wc; }

スレッドプールの実行プロセス

特定の実行ロジックは、主にThreadPoolExecutorのexecute()メソッドにあります。ソースコードは次のとおりです。

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        // ctl对象功能很强大,其高3位代表线程池的状态,低29位代表线程池中的线程数量
        int c = ctl.get();
        // 如果当前线程池中线程数量小于核心线程数,则新建一个线程并将当前任务直接赋予该线程执行
        if (workerCountOf(c) < corePoolSize) {
            // 如果新建线程成功则直接返回
            if (addWorker(command, true))
                return;
            // 到这一步说明新建失败,可能是线程池意外关闭或者是由于并发的原因导致当前线程数大于等于核心线程数了,重新获取ctl对象
            c = ctl.get();
        }
        // 如果当前线程处于运行态并且任务入队列成功,则继续执行下面的逻辑
        if (isRunning(c) && workQueue.offer(command)) {
            // 这里需要再次确认线程池是否仍处于运行态
            int recheck = ctl.get();
            // 如果非运行态则需要删除队列中的任务,然后拒绝该任务
            if (! isRunning(recheck) && remove(command))
                reject(command);
            // 确保线程池中仍有运行的线程,如果都未存活则新建一个线程且不指定任务参数,让该线程自行去队列中获取任务执行
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 如果处于非运行态或者入队列不成功(队列满了),尝试扩容线程池线程数量至maxPoolSize,若扩容失败,则拒绝该任务
        else if (!addWorker(command, false))
            reject(command);
    }

簡単に要約すると、主に3つのステップに分かれています。

  1. まず、現在実行中のスレッドの数がコアスレッドの数より少ないかどうかを判断します。少ない場合は、タスクを処理するための新しいスレッドを作成し、処理が成功した場合は直接戻ります。それ以外の場合は、引き続き判断します。
  2. 現在のスレッドプールが実行されているかどうか、およびタスクが正常にキューに入れられているかどうかを確認します。実行されている場合は、プールに実行中のスレッドがまだあることを再確認します。
  3. キューへの2番目のステップが失敗した場合、スレッドプールはスレッドの最大数まで拡張しようとします。失敗した場合、タスクは拒否されます。

上記の手順1と3はスレッドを追加するときにロックする必要があるため、パフォーマンスに影響します。したがって、スレッドプールのほとんどの操作は2番目の手順で実行されます(サポートするコアスレッドの数に依存します)。ブロッキングキューは適合できなくなりました。上記のコードから、コアスレッドの数が定義されていても、corePoolSizeスレッドの数は事前に作成されていませんが、毎回ロックが追加されていることがわかります。したがって、パフォーマンスを考慮して、prestartAllCoreThreads()を呼び出すことができます。 corePoolSizeスレッド数を事前に開始するメソッド。

新しいスレッドを作成するためのメソッドaddWorker()を見てみましょう。 

private boolean addWorker(Runnable firstTask, boolean core) {
        // 首先是一个外层死循环
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // 检查当前线程池是否处于非运行态,同时确保队列中任务数不为空
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;
            // 内存死循环修改运行的线程数量
            for (;;) {
                int wc = workerCountOf(c);
                // core参数确保不会超过线程池设定的值
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                // 采用CAS算法将线程数+1,如果成功则直接跳出外循环,失败主要是因为并发修改导致,那么则再次内循环判断
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                // 确保线程池运行状态没变,若发生改变,则从外循环开始判断
                c = ctl.get(); 
                if (runStateOf(c) != rs)
                    continue retry;
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            // 新建Worker内部类时主要干了两件事,一个是设置AQS同步锁标识为-1,另一个是调用线程工厂创建线程并赋值给Worker的成员变量
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                // 在往workers这个线程集合增加线程时需要进行加锁
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // 此处需要进行二次检查,防止线程池被关闭等异常情况
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        // 如果当前线程已经启动,处于活跃态则抛异常
                        if (t.isAlive())
                            throw new IllegalThreadStateException();
                        // workers是一个HashSet集合
                        workers.add(w);
                        int s = workers.size();
                        // 设置最大池大小,同时标识任务线程增加成功,即 workerAdded 设为true
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                // 如果任务线程成功增加,则在此处启动线程
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

非常に長いようですが、実際には異常な状況を防ぐためにいくつかの基本的な判断を行っています。簡単な要約:

  1. まず、実行状態にあり、現在実行中のスレッドの数が上限を下回っているかどうかを判断します。上限を下回っている場合は、実行中のスレッドの数が最初にCASアルゴリズムによって追加されます。
  2. CAS操作が成功したら、新しいワーカースレッドを作成し、ロックしてスレッドコレクションに追加してから、スレッドを開始します。

次に、Worker内部クラスを確認します。このクラスは、主にスレッドの中断を制御するためにタスクスレッドをラップします。つまり、スレッドプールが閉じられると、対応するスレッドタスクを中断する必要があります。ここで説明する中断は、 workQueueからのタスクgetTask()中断することしかできません。つまり、スレッドが実際に実行を開始した後にのみ中断が許可されるため、初期化中のロック状態は負の値(-1)になります。

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        /**
         * This class will never be serialized, but we provide a
         * serialVersionUID to suppress a javac warning.
         */
        private static final long serialVersionUID = 6138294804551838833L;

        /** 由线程工厂所创建的线程对象 */
        final Thread thread;
        /** 业务任务对象 */
        Runnable firstTask;
        /** 当前线程所执行任务的计数 */
        volatile long completedTasks;

        /**
         * 初始化方法,主要是设置AQS的同步状态private volatile int state,是一个计数器,大于0代表锁已经被获取,设为-1后即禁止中断
         */
        Worker(Runnable firstTask) {
            setState(-1); // 将lock标识设为-1,
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        public void run() {
            runWorker(this);
        }

        /**
         * 下面的都是与锁相关的方法,state为0代表锁未被获取,1代表锁已经被获取
         */
        protected boolean isHeldExclusively() {
            return getState() != 0;
        }

        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        public void lock()        { acquire(1); }
        public boolean tryLock()  { return tryAcquire(1); }
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }

        void interruptIfStarted() {
            Thread t;
            // 控制中断主要就是体现在中断前会判断 getState() >= 0
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
    }

Workerクラス自体がRunnableを実装し、AbstractQueuedSynchronizerを継承するため、実行可能タスクであるだけでなく、ロックの効果でもあります。ロックは単純な非再入可能ロックであることに注意してください。

最後に、主にスレッドプールでビジネスタスクを実行するメソッドrunWorker()を見てください。

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        // 将state置为0,允许线程中断
        w.unlock(); 
        boolean completedAbruptly = true;
        try {
            // task为上层透传进来的指定业务线程,若为空则循环通过getTask()获取任务执行
            while (task != null || (task = getTask()) != null) {
                // 这里的加锁不是为了防止并发,而是为了在shutdown()时不终止正在运行的任务
                w.lock();
                // 双重检查防止线程池状态不大于stop且未被设为中断标识
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    // 在执行业务代码之前执行,由子类实现
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        // 在执行业务代码之后执行,由子类实现
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            // 处理业务线程抛异常的情况
            processWorkerExit(w, completedAbruptly);
        }
    }

 

組み込みのスレッドプール

JDKには4つのスレッドプールが組み込まれており、それらはすべてExecutorsファクトリクラスを介して作成されます。この方法でスレッドプールを作成することはお勧めしません。以下にその理由を紹介します。

newFixedThreadPool

これは固定長のスレッドプールです

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

名前が示すように、固定長のスレッドプールが作成されます。スレッドプール内のコアスレッドの数はスレッドの最大数と同じであるため、非コアスレッドの存続時間パラメーターは無意味です。最大の問題は選択されたブロッキングキューは無制限です。無限に追加と追加を続け、最終的にメモリをバーストします。これは通常、負荷が高く、指定されたスレッド数を制限する必要があるサーバーに使用されます。

newSingleThreadExecutor

これはシングルスレッドのスレッドプールです

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

パラメータを見ると、初期化されるスレッドは1つだけであり、プールで最初から最後まで動作しているスレッドは1つだけであることがわかります。これは非常に哀れなことのように聞こえます。唯一の問題は、ブロッキングキューが無制限であり、バーストする可能性があることです。通常、厳密な要件に使用されます。タスクの実行順序を制御するシナリオ。

newCachedThreadPool

これはキャッシュ可能なスレッドプールです

 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

コアスレッドの数は0であり、スレッドの最大数は無限になる傾向があります。つまり、すべての非コアスレッドがタスクの処理に使用され、各スレッドの存続時間は60秒です。同期キューが使用されることに注意してください。ここでは、つまり、タスクのみが配信され、保存されません。タスクは、タスクが発生したら、アイドル状態のスレッドがあるかどうかを確認し、存在しない場合は、新しいスレッドを作成します。タスク要求のレートがスレッド処理の速度、スレッド数が増加し、最終的にメモリがバーストします。通常、同時実行に使用されます。60秒間アイドル状態のスレッドがリサイクルされるため、短期間の小さなタスクを多数実行します。スレッドプール長時間アイドル状態のままであると、リソースを占有しません。

newScheduledThreadPool

これも固定長のスレッドプールですが、タイミングと定期的なタスクの実行をサポートしています

public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory, handler);
    }

スレッドの最大数はまだ無限であるため、ある程度のメモリバーストの問題が発生することがわかります。キューの使用では、遅延ブロッキングキューDelayedWorkQueueが選択され、特定のタスクの実行も多くなります。複雑です。

 

おすすめ

転載: blog.csdn.net/m0_38001814/article/details/107729317