简体   繁体   English

Java ExecutorService:所有递归创建的任务的awaitTermination

[英]Java ExecutorService: awaitTermination of all recursively created tasks

I use an ExecutorService to execute a task. 我使用ExecutorService执行任务。 This task can recursively create other tasks which are submitted to the same ExecutorService and those child tasks can do that, too. 该任务可以递归创建其他任务,这些其他任务提交给相同的ExecutorService而那些子任务也可以这样做。

I now have the problem that I want to wait until all the tasks are done (that is, all tasks are finished and they did not submit new ones) before I continue. 我现在遇到的问题是,我要等到所有任务都完成(即所有任务都已完成并且它们没有提交新任务)后再继续。

I cannot call ExecutorService.shutdown() in the main thread because this prevents new tasks from being accepted by the ExecutorService . 我无法在主线程中调用ExecutorService.shutdown() ,因为这会阻止ExecutorService接受新任务。

And Calling ExecutorService.awaitTermination() seems to do nothing if shutdown hasn't been called. 如果尚未调用shutdown则调用ExecutorService.awaitTermination()似乎无济于事。

So I am kinda stuck here. 所以我有点卡在这里。 It can't be that hard for the ExecutorService to see that all workers are idle, can it? 对于ExecutorService ,看到所有工作人员都空闲并不难,不是吗? The only inelegant solution I could come up with is to directly use a ThreadPoolExecutor and query its getPoolSize() every once in a while. 我能想到的唯一优雅的解决方案是直接使用ThreadPoolExecutor并每隔一段时间查询其getPoolSize() Is there really no better way do do that? 真的没有更好的方法吗?

If number of tasks in the tree of recursive tasks is initially unknown, perhaps the easiest way would be to implement your own synchronization primitive, some kind of "inverse semaphore", and share it among your tasks. 如果最初不清楚递归任务树中的任务数量,则最简单的方法可能是实现自己的同步原语(某种“反信号量”),并在任务之间共享它。 Before submitting each task you increment a value, when task is completed, it decrements that value, and you wait until the value is 0. 在提交每个任务之前,您需要增加一个值,当任务完成时,它会减少该值,然后等待直到该值为0。

Implementing it as a separate primitive explicitly called from tasks decouples this logic from the thread pool implementation and allows you to submit several independent trees of recursive tasks into the same pool. 将其实现为从任务中显式调用的单独原语,可以将此逻辑与线程池实现分离开来,并允许您将多个独立的递归任务树提交到同一池中。

Something like this: 像这样:

public class InverseSemaphore {
    private int value = 0;
    private Object lock = new Object();

    public void beforeSubmit() {
        synchronized(lock) {
            value++;
        }
    }

    public void taskCompleted() {
        synchronized(lock) {
            value--;
            if (value == 0) lock.notifyAll();
        }
    }

    public void awaitCompletion() throws InterruptedException {
        synchronized(lock) {
            while (value > 0) lock.wait();
        }
    }
}

Note that taskCompleted() should be called inside a finally block, to make it immune to possible exceptions. 注意, taskCompleted()应该在finally块中调用,以使其不受可能的异常影响。

Also note that beforeSubmit() should be called by the submitting thread before the task is submitted, not by the task itself, to avoid possible "false completion" when old tasks are completed and new ones not started yet. 还要注意,应在提交任务之前由提交线程调用beforeSubmit() ,而不是由任务本身调用,以避免在旧任务完成而新任务尚未开始时可能出现的“错误完成”。

EDIT: Important problem with usage pattern fixed. 编辑:使用模式已修复的重要问题。

This really is an ideal candidate for a Phaser. 这确实是Phaser的理想候选人。 Java 7 is coming out with this new class. Java 7即将推出这一新类。 Its a flexible CountdonwLatch/CyclicBarrier. 它是灵活的CountdonwLatch / CyclicBarrier。 You can get a stable version at JSR 166 Interest Site . 您可以在JSR 166 Interest Site获得稳定版本。

The way it is a more flexible CountdownLatch/CyclicBarrier is because it is able to not only support an unknown number of parties (threads) but its also reusable (thats where the phase part comes in) CountdownLatch / CyclicBarrier更加灵活的方式是因为它不仅能够支持未知数目的参与者(线程),而且还可以重用(这就是阶段部分所在的位置)

For each task you submit you would register, when that task is completed you arrive. 对于您提交的每个任务,您都将进行注册,当该任务完成时,您便会到达。 This can be done recursively. 这可以递归完成。

Phaser phaser = new Phaser();
ExecutorService e = //

Runnable recursiveRunnable = new Runnable(){
   public void run(){
      //do work recursively if you have to

      if(shouldBeRecursive){
           phaser.register();
           e.submit(recursiveRunnable);
      }

      phaser.arrive();
   }
}

public void doWork(){
   int phase = phaser.getPhase();

   phaser.register();
   e.submit(recursiveRunnable);

   phaser.awaitAdvance(phase);
}

Edit: Thanks @depthofreality for pointing out the race condition in my previous example. 编辑:感谢@depthofreality指出我的上一个示例中的比赛条件。 I am updating it so that executing thread only awaits advance of the current phase as it blocks for the recursive function to complete. 我正在对其进行更新,以便正在执行的线程仅等待当前阶段的前进,因为它阻塞了递归函数的完成。

The phase number won't trip until the number of arrive s == register s. 相数直到arrive数s == register s才会跳闸。 Since prior to each recursive call invokes register a phase increment will happen when all invocations are complete. 由于在每个递归调用之前都进行register所以当所有调用完成时,将发生相位递增。

Wow, you guys are quick:) 哇,你们很快:)

Thank you for all the suggestions. 感谢您的所有建议。 Futures don't easily integrate with my model because I don't know how many runnables are scheduled beforehand. 期货不容易与我的模型集成,因为我不知道事先预定了多少可运行对象。 So if I keep a parent task alive just to wait for it's recursive child tasks to finish I have a lot of garbage laying around. 因此,如果我让父任务活着只是为了等待它的递归子任务完成,那么我周围就会有很多垃圾。

I solved my problem using the AtomicInteger suggestion. 我使用AtomicInteger建议解决了我的问题。 Essentially, I subclassed ThreadPoolExecutor and increment the counter on calls to execute() and decrement on calls to afterExecute(). 本质上,我将ThreadPoolExecutor子类化,并在对execute()的调用中增加计数器,在对afterExecute()的调用中减少计数器。 When the counter gets 0 I call shutdown(). 当计数器为0时,我调用shutdown()。 This seems to work for my problems, not sure if that's a generally good way to do that. 这似乎可以解决我的问题,不确定这样做是否是通常的好方法。 Especially, I assume that you only use execute() to add Runnables. 特别是,我假设您仅使用execute()添加Runnable。

As a side node: I first tried to check in afterExecute() the number of Runnables in the queue and the number of workers that are active and shutdown when those are 0; 作为副节点:我首先尝试检入afterExecute()队列中的Runnable数量,以及当它们为0时活动和关闭的工作程序的数量; but that didn't work because not all Runnables showed up in the queue and the getActiveCount() didn't do what I expected either. 但这不起作用,因为并非所有Runnable都出现在队列中,并且getActiveCount()也不符合我的预期。

Anyhow, here's my solution: (if anybody finds serious problems with this, please let me know:) 无论如何,这是我的解决方案:(如果有人发现严重问题,请告诉我:)

public class MyThreadPoolExecutor extends ThreadPoolExecutor {

    private final AtomicInteger executing = new AtomicInteger(0);

    public MyThreadPoolExecutor(int coorPoolSize, int maxPoolSize, long keepAliveTime,
        TimeUnit seconds, BlockingQueue<Runnable> queue) {
        super(coorPoolSize, maxPoolSize, keepAliveTime, seconds, queue);
    }


    @Override
    public void execute(Runnable command) {
        //intercepting beforeExecute is too late!
        //execute() is called in the parent thread before it terminates
        executing.incrementAndGet();
        super.execute(command);
    }


    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        int count = executing.decrementAndGet();
        if(count == 0) {
            this.shutdown();
        }
    }

}

You could create your own thread pool which extends ThreadPoolExecutor . 您可以创建自己的线程池,以扩展ThreadPoolExecutor You want to know when a task has been submitted and when it completes. 您想知道任务何时提交以及任务何时完成。

public class MyThreadPoolExecutor extends ThreadPoolExecutor {
    private int counter = 0;

    public MyThreadPoolExecutor() {
        super(1, 1, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    }

    @Override
    public synchronized void execute(Runnable command) {
        counter++;
        super.execute(command);
    }

    @Override
    protected synchronized void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        counter--;
        notifyAll();
    }

    public synchronized void waitForExecuted() throws InterruptedException {
        while (counter == 0)
            wait();
    }
}

对您的任务使用Future (而不是提交Runnable ),回调将在完成时更新其状态,因此您可以使用Future.isDone来跟踪所有任务的状态。

I must say, that solutions described above of problem with recursive calling task and wait for end suborder tasks doesn't satisfy me. 我必须说,上述递归调用任务和等待结束子任务的问题的解决方案令我不满意。 There is my solution inspired by original documentation from Oracle there: CountDownLatch and example there: Human resources CountDownLatch . 我的解决方案受到Oracle原始文档的启发: CountDownLatch和那里的示例: 人力资源CountDownLatch

The first common thread in process in instance of class HRManagerCompact has waiting latch for two daughter's threads, wich has waiting latches for their subsequent 2 daughter's threads... etc. 类HRManagerCompact实例中正在处理的第一个公共线程具有等待闩锁的两个子线程,而拥有等待闩锁的后续的两个子线程...等等。

Of course, latch can be set on the different value than 2 (in constructor of CountDownLatch), as well as the number of runnable objects can be established in iteration ie ArrayList, but it must correspond (number of count downs must be equal the parameter in CountDownLatch constructor). 当然,闩锁可以设置为不同于2的值(在CountDownLatch的构造函数中),并且可以在迭代中建立可运行对象的数量(即ArrayList),但是它必须是对应的(递减计数必须等于参数在CountDownLatch构造函数中)。

Be careful, the number of latches increases exponentially according restriction condition: 'level.get() < 2', as well as the number of objects. 注意,锁存器的数量会根据限制条件'level.get()<2'以及对象的数量呈指数增长。 1, 2, 4, 8, 16... and latches 0, 1, 2, 4... As you can see, for four levels (level.get() < 4) there will be 15 waiting threads and 7 latches in the time, when peak 16 threads are running. 1、2、4、8、16 ...和锁存器0、1、2、4 ...如您所见,对于四个级别(level.get()<4),将有15个等待线程和7个锁存器在当时,高峰16个线程正在运行。

package processes.countdownlatch.hr;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/** Recursively latching running classes to wait for the peak threads
 *
 * @author hariprasad
 */
public class HRManagerCompact extends Thread {
  final int N = 2; // number of daughter's tasks for latch
  CountDownLatch countDownLatch;
  CountDownLatch originCountDownLatch;
  AtomicInteger level = new AtomicInteger(0);
  AtomicLong order = new AtomicLong(0); // id latched thread waiting for

  HRManagerCompact techLead1 = null;
  HRManagerCompact techLead2 = null;
  HRManagerCompact techLead3 = null;

// constructor
public HRManagerCompact(CountDownLatch countDownLatch, String name,
    AtomicInteger level, AtomicLong order){
  super(name);
  this.originCountDownLatch=countDownLatch;
  this.level = level;
  this.order = order;
 }

 private void doIt() {
    countDownLatch = new CountDownLatch(N);
    AtomicInteger leveli = new AtomicInteger(level.get() + 1);
    AtomicLong orderi = new AtomicLong(Thread.currentThread().getId());
    techLead1 = new HRManagerCompact(countDownLatch, "first", leveli, orderi);
    techLead2 = new HRManagerCompact(countDownLatch, "second", leveli, orderi);
    //techLead3 = new HRManagerCompact(countDownLatch, "third", leveli);

    techLead1.start();
    techLead2.start();
    //techLead3.start();

    try {
     synchronized (Thread.currentThread()) { // to prevent print and latch in the same thread
       System.out.println("*** HR Manager waiting for recruitment to complete... " + level + ", " + order + ", " + orderi);
       countDownLatch.await(); // wait actual thread
     }
     System.out.println("*** Distribute Offer Letter, it means finished. " + level + ", " + order + ", " + orderi);
    } catch (InterruptedException e) {
     e.printStackTrace();
    }
  }

 @Override
 public void run() {
  try {
   System.out.println(Thread.currentThread().getName() + ": working... " + level + ", " + order + ", " + Thread.currentThread().getId());
   Thread.sleep(10*level.intValue());
   if (level.get() < 2) doIt();
   Thread.yield();
  }
  catch (Exception e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  /*catch (InterruptedException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }*/
  // TODO Auto-generated method stub
  System.out.println("--- " +Thread.currentThread().getName() + ": recruted " + level + ", " + order + ", " + Thread.currentThread().getId());
  originCountDownLatch.countDown(); // count down
 }

 public static void main(String args[]){
  AtomicInteger levelzero = new AtomicInteger(0);
  HRManagerCompact hr = new HRManagerCompact(null, "zero", levelzero, new AtomicLong(levelzero.longValue()));
  hr.doIt();
 }
}

Possible commented output (with some probability): 可能的评论输出(有可能):

first: working... 1, 1, 10 // thread 1, first daughter's task (10)
second: working... 1, 1, 11 // thread 1, second daughter's task (11)
first: working... 2, 10, 12 // thread 10, first daughter's task (12)
first: working... 2, 11, 14 // thread 11, first daughter's task (14)
second: working... 2, 11, 15 // thread 11, second daughter's task (15)
second: working... 2, 10, 13 // thread 10, second daughter's task (13)
--- first: recruted 2, 10, 12 // finished 12
--- first: recruted 2, 11, 14 // finished 14
--- second: recruted 2, 10, 13  // finished 13 (now can be opened latch 10)
--- second: recruted 2, 11, 15  // finished 15 (now can be opened latch 11)
*** HR Manager waiting for recruitment to complete... 0, 0, 1
*** HR Manager waiting for recruitment to complete... 1, 1, 10
*** Distribute Offer Letter, it means finished. 1, 1, 10 // latch on 10 opened
--- first: recruted 1, 1, 10 // finished 10
*** HR Manager waiting for recruitment to complete... 1, 1, 11
*** Distribute Offer Letter, it means finished. 1, 1, 11 // latch on 11 opened
--- second: recruted 1, 1, 11  // finished 11 (now can be opened latch 1)
*** Distribute Offer Letter, it means finished. 0, 0, 1  // latch on 1 opened

The only inelegant solution I could come up with is to directly use a ThreadPoolExecutor and query its getPoolSize() every once in a while. 我能想到的唯一优雅的解决方案是直接使用ThreadPoolExecutor并每隔一段时间查询其getPoolSize()。 Is there really no better way do do that? 真的没有更好的方法吗?

You have to use shutdown() , awaitTermination() and shutdownNow() methods in a proper sequence. 您必须按正确的顺序使用shutdown() , awaitTermination() and shutdownNow()方法。

shutdown() : Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted. shutdown() :启动有序关闭,在该关闭中执行先前提交的任务,但不接受任何新任务。

awaitTermination() :Blocks until all tasks have completed execution after a shutdown request, or the timeout occurs, or the current thread is interrupted, whichever happens first. awaitTermination() :阻塞直到关闭请求后所有任务完成执行,或者发生超时,或者当前线程被中断(以先发生者为准)。

shutdownNow() : Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution. shutdownNow() :尝试停止所有正在执行的任务,暂停正在等待的任务的处理,并返回正在等待执行的任务的列表。

Recommended way from oracle documentation page of ExecutorService : ExecutorService的 oracle文档页面推荐的方法:

 void shutdownAndAwaitTermination(ExecutorService pool) {
   pool.shutdown(); // Disable new tasks from being submitted
   try {
     // Wait a while for existing tasks to terminate
     if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
       pool.shutdownNow(); // Cancel currently executing tasks
       // Wait a while for tasks to respond to being cancelled
       if (!pool.awaitTermination(60, TimeUnit.SECONDS))
           System.err.println("Pool did not terminate");
     }
   } catch (InterruptedException ie) {
     // (Re-)Cancel if current thread also interrupted
     pool.shutdownNow();
     // Preserve interrupt status
     Thread.currentThread().interrupt();
   }

You can replace if condition with while condition in case of long duration in completion of tasks as below: 如果长时间完成任务,则可以将if条件替换为while条件,如下所示:

Change 更改

if (!pool.awaitTermination(60, TimeUnit.SECONDS))

To

 while(!pool.awaitTermination(60, TimeUnit.SECONDS)) {
     Thread.sleep(60000);
 }  

You can refer to other alternatives (except join() , which can be used with standalone thread ) in : 您可以在以下内容中引用其他替代方法( join()除外,该方法可以与独立线程一起使用):

wait until all threads finish their work in java 等到所有线程在Java中完成工作

Use CountDownLatch . 使用CountDownLatch Pass the CountDownLatch object to each of your tasks and code your tasks something like below. 将CountDownLatch对象传递给每个任务,并对任务进行编码,如下所示。

public void doTask() {
    // do your task
    latch.countDown(); 
}

Whereas the thread which needs to wait should execute the following code: 而需要等待的线程应执行以下代码:

public void doWait() {
    latch.await();
}

But ofcourse, this assumes you already know the number of child tasks so that you could initialize the latch's count. 但是,当然,这假设您已经知道子任务的数量,以便可以初始化闩锁的计数。

You could use a runner that keeps track of running threads: 您可以使用运行程序来跟踪正在运行的线程:

Runner runner = Runner.runner(numberOfThreads);

runner.runIn(2, SECONDS, callable);
runner.run(callable);


// blocks until all tasks are finished (or failed)
runner.waitTillDone();


// and reuse it
runner.runRunnableIn(500, MILLISECONDS, runnable);


runner.waitTillDone();


// and then just kill it
runner.shutdownAndAwaitTermination();

to use it you just add a dependency: 要使用它,您只需添加一个依赖项:

compile 'com.github.matejtymes:javafixes:1.3.0' 编译'com.github.matejtymes:javafixes:1.3.0'

(mea culpa: its a 'bit' past my bedtime ;) but here's a first attempt at a dynamic latch): (是罪魁祸首:这是我睡前的“一点点”;)但这是动态锁存的首次尝试):

package oss.alphazero.sto4958330;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class DynamicCountDownLatch {
    @SuppressWarnings("serial")
    private static final class Sync extends AbstractQueuedSynchronizer {
        private final CountDownLatch toplatch;
        public Sync() {
            setState(0);
            this.toplatch = new CountDownLatch(1);
        }

        @Override
        protected int tryAcquireShared(int acquires){
            try {
                toplatch.await();
            } 
            catch (InterruptedException e) {
                throw new RuntimeException("Interrupted", e);
            }
            return getState() == 0 ? 1 : -1;
        }
        public boolean tryReleaseShared(int releases) {
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc)) 
                    return nextc == 0;
            }
        }
        public boolean tryExtendState(int acquires) {
            for (;;) {
                int s = getState();
                int exts = s+1;
                if (compareAndSetState(s, exts)) {
                    toplatch.countDown();
                    return exts > 0;
                }
            }
        }
    }
    private final Sync sync;
    public DynamicCountDownLatch(){
        this.sync = new Sync();
    }
    public void await() 
        throws InterruptedException   
    {
        sync.acquireSharedInterruptibly(1);
    }

    public boolean await(long timeout, TimeUnit   unit) 
        throws InterruptedException   
    {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
    public void countDown() {
        sync.releaseShared(1);
    }
    public void join() {
        sync.tryExtendState(1);
    }
}

This latch introduces a new method join() to the existing (cloned) CountDownLatch API, which is used by tasks to signal their entry into the larger task group. 此闩锁将新方法join()引入到现有的(克隆的)CountDownLatch API中,任务使用该方法来表示它们已进入较大的任务组。

The latch is pass around from parent Task to child Task. 闩锁从父任务传递到子任务。 Each task would, per Suraj's pattern, first 'join()' the latch, do its task(), and then countDown(). 按照Suraj的模式,每个任务将首先“闩锁” join(),执行其task(),然后执行countDown()。

To address situations where the main thread launches the task group and then immediately awaits() -- before any of the task threads have had a chance to even join() -- the topLatch is used int inner Sync class. 为了解决主线程启动任务组然后立即进行awaits()的情况-在任何任务线程甚至没有机会加入join()之前,将topLatch用于内部Sync类。 This is a latch that will get counted down on each join(); 这是一个锁存器,将在每个join()上计数。 only the first countdown is of course significant, as all subsequent ones are nops. 当然,只有第一个倒计时很重要,因为所有后续倒数都是点数。

The initial implementation above does introduce a semantic wrinkle of sorts since the tryAcquiredShared(int) is not supposed to be throwing an InterruptedException but then we do need to deal with the interrupt on the wait on the topLatch. 上面的初始实现确实引入了某种语义上的折皱,因为tryAcquiredShared(int)不应引发InterruptedException,但是我们确实需要在topLatch的等待中处理中断。

Is this an improvement over OP's own solution using Atomic counters? 这是对OP自己使用原子计数器的解决方案的改进吗? I would say probably not IFF he is insistent upon using Executors, but it is, I believe, an equally valid alternative approach using the AQS in that case, and, is usable with generic threads as well. 我想说的可能不是IFF,他坚持使用Executor,但我认为,在那种情况下,它是使用AQS的同等有效的替代方法,并且也可用于通用线程。

Crit away fellow hackers. 甩掉其他黑客。

If you want to use JSR166y classes - eg Phaser or Fork/Join - either of which might work for you, you can always download the Java 6 backport of them from: http://gee.cs.oswego.edu/dl/concurrency-interest/ and use that as a basis rather than writing a completely homebrew solution. 如果您想使用JSR166y类(例如Phaser或Fork / Join),它们中的任何一个都可能对您有用,则可以始终从以下网站下载它们的Java 6反向端口: http : //gee.cs.oswego.edu/dl/concurrency -interest /,并以此为基础,而不是编写完全自制的解决方案。 Then when 7 comes out you can just drop the dependency on the backport and change a few package names. 然后,当7出现时,您可以只删除对backport的依赖关系并更改一些软件包名称。

(Full disclosure: We've been using the LinkedTransferQueue in prod for a while now. No issues) (完整披露:我们已经在产品中使用LinkedTransferQueue已有一段时间了。没有问题)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 Java ExecutorService - 如果awaitTermination()失败怎么办? - Java ExecutorService - What if awaitTermination() fails? Java - 停止ExecutorService中的所有任务 - Java - stopping all tasks in ExecutorService Java ExecutorService 一次性拉取所有任务 - Java ExecutorService pulls tasks all at once Java ExecutorService获取所有任务的反馈 - Java ExecutorService get feedback for all tasks Java的ExecutorService.awaitTermination与Future.get( <TimeOut> ) - Java's ExecutorService.awaitTermination vs Future.get(<TimeOut>) java ExecutorService.awaitTermination() 是否阻塞主线程并等待? - Does java ExecutorService.awaitTermination() block the main thread and wait? ExecutorService awaitTermination卡住了 - ExecutorService awaitTermination gets stuck 从不同于创建ExecutorService的线程中调用ExecutorService.shutdown()和awaitTermination()是否安全? - Is it safe to call ExecutorService.shutdown() and awaitTermination() from a different thread than created the ExecutorService? 如何使用ExecutorService递归调度任务 - How to schedule tasks recursively using ExecutorService java ExecutorService newSingleThreadExecutor 是否仅使用一个线程执行所有任务? - Is java ExecutorService newSingleThreadExecutor performs all the tasks using only one Thread?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM