スレッドプールのピット-インスタンスメンバーまたはメソッドのローカル変数としてのエラー


この文書の目次:
  • 1. 概要
  • 2. 検証
  • 3. 剖析
  • 4. 小結

  • 1.概要
    スレッドプールは、スレッドを多重化し、スレッド作成の破棄時間とリソース消費を削減し、プログラムタスクのスループットを向上させることができます.
    スレッドがグローバルで使用されるリソースに属するように、スレッドプールも一般的にグローバルであり、アプリケーションプロセス全体のスレッド多重化を効率的に管理します.デザイナーは、通常、スレッドプールをクラスの静的メンバーまたは単一のメンバーとして、プロセス全体のライフサイクルで生存します.
    しかし、このようなコードは例外的に見られた.
    たとえば、メソッドボディにローカル変数として配置されます.
    private static void sampleFunc() {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    ...
                }
            });
        }
    }

    または、ライフサイクルの短いオブジェクトにメンバー変数として挿入します.
    public class SampleClass {
        ...
        private final ExecutorService mExecutor = Executors.newFixedThreadPool(2);
        ...
    }

    これらのスレッドプールの使用は正常に見え、深刻な問題が隠されています.
    オブジェクトインスタンスが使用されなくなったり、メソッドが実行されなくなったりすると、スレッドが解放され、スレッドプールが閉じられますか?
    スレッドプールによって表現が異なります.コアスレッド数が設定されているかどうかを主に見ます.
  • コアスレッド数、例えばnewCachedThreadPoolが設定されていない場合、オンラインスレッドプールのスレッド空き時間が60 sに達すると、スレッドは閉じられ、すべてのスレッドが閉じた後、スレッドプールもそれに応じて回収を閉じます.
  • コアスレッド数、例えばnewSingleThreadExecutorおよびnewFixedThreadPoolが設定されている場合、アクティブに閉じるか、コアスレッドのタイムアウト時間を設定していないと、コアスレッドは閉じられず、このスレッドプールは回収されません.

  • 2.検証
    Demoを設計して検証します.
  • JDKが提供する単一スレッドプールを選択します.このスレッドプールのコアスレッド数は1です.
  • スレッドプールは、オブジェクトのメンバー変数として使用されます.
  • このクラスのインスタンスオブジェクトはメソッドボディで実行され、外部は
  • を参照していない.
  • 最終呼び出しSystem.gcアクティブ回収
  • しばらく待ってから、プロセスのスタック情報を印刷し、関連するクラスを表示します.

  • 上記のようにSimpleClassを作成しました.
    public class SimpleClass {
        private final int mIndex;
        private Executor mExecutors = Executors.newSingleThreadExecutor();
    
        public SimpleClass(int index) {
            mIndex = index;
        }
    
        public void runTask() {
            mExecutors.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("[" + mIndex + "] execute");
                }
            });
        }
    }

    そしてmainメソッドで実行
    public class TestThreadLife {
        public static void main(String[] args) {
            test();
            System.gc();
        }
        private static void test() {
            for (int i = 0; i < 10; i++) {
                new SimpleClass(i).runTask();
            }
        }
    }

    しばらくすると、JDKのツールを使用して、現在生存しているオブジェクト情報を取得します.
  • jps印刷取得プロセス番号4540
  • は、jmap -histo 4540を呼び出してスタック内のオブジェクト情報を読み出す.

  • コンソールには、次のようなレコードが表示されます.
    E:\code\workspace-demo\TestJava>jmap -histo 4540
    
     num     #instances         #bytes  class name
    ----------------------------------------------
    ...
      10:            20           7520  java.lang.Thread
    ...
      52:            10            480  java.util.concurrent.LinkedBlockingQueue
      53:            10            480  java.util.concurrent.ThreadPoolExecutor$Worker
      54:            30            480  java.util.concurrent.locks.ReentrantLock
    ...  
     218:             1             16  com.intellij.rt.execution.application.AppMain$1
     219:             1             16  concurrent.threadpool.TestThreadLife.SimpleClass
    ... 
    Total          9740       33368472

    しばらくの間、アクティブコールGCを加えると、10個のSimpleClassインスタンスはほぼ回収されたが、10個のThreadPoolExecutorインスタンスは依然として存在する.
    これは、コアスレッドを持つThreadPoolExecutorが、スレッドのタイムアウトをアクティブに解放したり設定したりしていない場合、メンバー変数に配置すると、オブジェクトインスタンスの漏洩が発生します.
    同様に,メソッド体に局所変数を置くという問題もある.
    3.剖析
    どうしてこんな現象があるの?
    スレッドプールが回収できないのは、スレッドプールの参照が内部クラスWorkerによって保持されているためです.一方,Workerとスレッドは1つ1つに対応しており,Threadの強化であるため,本質的にはスレッドが解放されていないためである.
    では、タスクキューは空で、外部からもタスクが来ていません.スレッドはなぜまだ解放されていませんか.
    ThreadPoolExecutorのrunWorkerの方法を見てください.
    final void runWorker(Worker w) {
            Thread wt = Thread.currentThread();
            Runnable task = w.firstTask;
            w.firstTask = null;
            w.unlock(); // allow interrupts
            boolean completedAbruptly = true;
            try {
                while (task != null || (task = getTask()) != null) {
                    w.lock();
                    // If pool is stopping, ensure thread is interrupted;
                    // if not, ensure thread is not interrupted.  This
                    // requires a recheck in second case to deal with
                    // shutdownNow race while clearing interrupt
                    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);
            }
        }

    スレッド終了processWorkerExitを実行するには、次のような状況が必要です.
  • スレッドプールの状態>=STOP
  • getTask空のタスク
  • を取得
    最初の条件は、スレッドプールの状態がSTOPに達するには、shutdownまたはshutdownNowメソッドを呼び出す必要があります.
    2つ目の条件は、getTaskが空のタスクを取得し、getTaskのコードを見続けます.
        private Runnable getTask() {
            boolean timedOut = false; // Did the last poll() time out?
    
            for (;;) {
                int c = ctl.get();
                int rs = runStateOf(c);
    
                // Check if queue empty only if necessary.
                if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                    decrementWorkerCount();
                    return null;
                }
    
                int wc = workerCountOf(c);
    
                // Are workers subject to culling?
                boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
    
                if ((wc > maximumPoolSize || (timed && timedOut))
                    && (wc > 1 || workQueue.isEmpty())) {
                    if (compareAndDecrementWorkerCount(c))
                        return null;
                    continue;
                }
    
                try {
                    Runnable r = timed ?
                        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                        workQueue.take();
                    if (r != null)
                        return r;
                    timedOut = true;
                } catch (InterruptedException retry) {
                    timedOut = false;
                }
            }
        }

    タスクキューは、タスクを取得するための2つの方法を提供するブロックキューBlockingQueueを使用します.
  • pollでは、タイムアウト時間を設定でき、タイムアウト後に空のタスクが得られます.
  • take、タスクが現れるまでブロックします.

  • 上のgetTaskメソッドから、
  • 現在のスレッド数がコアスレッドより大きい場合、pollが呼び出され、タイムアウト後に空のタスクに戻ります.
  • 現在のスレッド数がコアスレッド以下であり、allowCoreThreadTimeOutメソッドが呼び出されてコアスレッドがタイムアウトして閉じることができる場合もpollが呼び出され、タイムアウト後に空のタスクに戻る.
  • 他の場合、takeブロック待ちを呼び出す.

  • 単一のコアスレッドのスレッドプールを使用していますが、タスクがない場合、コアスレッドはgetTaskにあります.ブロックキューBlockingQueueを呼び出すtakeメソッドは、取得待ちタスクをブロックし、スレッドプールに含まれるコアスレッドが閉じられずに回収されます.
    4.まとめ
    上記のようにスレッドプールを設定すると,スレッドプールのローカルアプリケーションとして理解できる.
    ローカルスレッドプールでできることは、グローバルスレッドプールでもできるので、このような方法はお勧めしません.また、グローバル・インスタンスのスレッド・プールは、ライフサイクルとプロセスが一致するため、スレッド・プールを閉じる問題を考慮する必要はありません.
    ビジネスシーンでこのように使用しなければならない場合、スレッドプールにコアスレッドがある場合は、オブジェクトの漏洩を防ぐために2つのことに注意してください.
  • コアスレッドにタイムアウト時間を設定します.
  • は、スレッドプールを閉じるためにshutdownまたはshutdownNowをアクティブに呼び出す.