我使用ExecutorService
来执行任务。此任务可以递归地创建提交给同一ExecutorService
的其他任务,这些子任务也可以这样做。
我现在遇到的问题是,我要等到所有任务完成(也就是说,所有任务都已完成,并且他们没有提交新的任务),然后再继续。
我无法在主线程中调用ExecutorService.shutdown()
,因为这会阻止ExecutorService
接受新任务。
如果ExecutorService.awaitTermination()
未被调用,则调用shutdown
似乎无效。
所以我有点被困在这里。 ExecutorService
看到所有工人都处于闲置状态并不难,是吗?我能想出的唯一不合理的解决方案是直接使用ThreadPoolExecutor
并每隔一段时间查询一次getPoolSize()
。这样做真的没有更好的方法吗?
答案 0 :(得分:17)
这确实是Phaser的理想候选人。 Java 7将推出这个新类。它灵活的CountdonwLatch / CyclicBarrier。您可以在JSR 166 Interest Site获得稳定版本。
它是一种更灵活的CountdownLatch / CyclicBarrier,因为它不仅能够支持未知数量的聚会(线程),而且还可以重复使用(相位部分进入的地方)
对于您提交的每项任务,您都会注册,当该任务完成后,您将到达。这可以递归完成。
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);
}
编辑:感谢@depthofreality指出我之前示例中的竞争条件。我正在更新它,以便执行线程只等待当前阶段的进展,因为它阻止递归函数完成。
在arrive
s == register
s的数量之前,阶段编号不会跳闸。由于在每次递归调用之前调用register
,所有调用完成后都会发生相位增量。
答案 1 :(得分:16)
如果递归任务树中的任务数最初是未知的,也许最简单的方法是实现自己的同步原语,某种“逆信号量”,并在您的任务中共享它。在提交每个任务之前,您需要递增一个值,当任务完成时,它会递减该值,并等到该值为0.
将它实现为从任务显式调用的单独原语将此逻辑与线程池实现分离,并允许您将几个独立的递归任务树提交到同一个池中。
这样的事情:
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();
}
}
}
请注意,应在taskCompleted()
块内调用finally
,以使其免受可能的异常的影响。
另请注意,在提交任务之前,提交线程应调用beforeSubmit()
,而不是由任务本身调用,以避免在旧任务完成且新的任务尚未启动时可能出现“错误完成”。 / p>
编辑:修复了使用模式的重要问题。
答案 2 :(得分:6)
感谢您提出的所有建议。期货不容易与我的模型集成,因为我不知道预先安排了多少runnables。因此,如果我将父任务保持活着只是为了等待它的递归子任务完成,那我就会产生大量的垃圾。
我使用AtomicInteger建议解决了我的问题。本质上,我将ThreadPoolExecutor子类化,并在调用execute()时递增计数器,并在调用afterExecute()时递减。当计数器变为0时,我调用shutdown()。这似乎对我的问题有用,不确定这是否是一种通常很好的方法。特别是,我假设你只使用execute()来添加Runnables。
作为一个侧节点:我首先尝试检查afterExecute()队列中Runnables的数量以及当它们为0时活动和关闭的worker数量;但这不起作用,因为并非所有Runnables都出现在队列中,并且getActiveCount()没有按照我的预期行事。
无论如何,这是我的解决方案:(如果有人发现此问题严重,请告诉我:)。
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();
}
}
}
答案 3 :(得分:2)
您可以创建自己的扩展ThreadPoolExecutor的线程池。您想知道任务何时提交以及何时完成。
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();
}
}
答案 4 :(得分:1)
使用Future执行任务(而不是提交Runnable
),回调会在完成时更新状态,因此您可以使用Future.isDone来跟踪所有内容的状态你的任务。
答案 5 :(得分:0)
使用CountDownLatch。 将CountDownLatch对象传递给您的每个任务,并将您的任务编码如下。
public void doTask() {
// do your task
latch.countDown();
}
而需要等待的线程应该执行以下代码:
public void doWait() {
latch.await();
}
但是,当然,这假设您已经知道子任务的数量,以便您可以初始化锁存器的数量。
答案 6 :(得分:0)
(mea culpa:我的睡觉时间过了一段时间;但这是第一次尝试动态锁定):
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);
}
}
此锁存器向现有(克隆的)CountDownLatch API引入了一个新方法join(),任务用它来表示它们进入较大的任务组。
闩锁从父任务传递到子任务。根据Suraj的模式,每个任务首先将'join()'锁存,执行其task(),然后执行countDown()。
要解决主线程启动任务组然后立即等待()的情况 - 在任何任务线程有机会连接()之前 - topLatch
使用int inner { {1}}课程。这是一个将在每个join()上倒计时的锁存器;只有第一次倒计时当然是重要的,因为所有后续倒计时都是nops。
上面的初始实现确实引入了各种语义皱纹,因为tryAcquiredShared(int)不应该抛出InterruptedException但是我们确实需要处理topLatch上等待的中断。
这是使用原子计数器改进OP自己的解决方案吗?我想说可能不是IFF他坚持使用Executors,但我认为,在这种情况下使用AQS是一种同样有效的替代方法,并且也适用于通用线程。
批评黑客同伴。
答案 7 :(得分:0)
如果你想使用JSR166y类 - 例如Phaser或Fork / Join - 其中任何一个都可能适用于您,您始终可以从http://gee.cs.oswego.edu/dl/concurrency-interest/下载它们的Java 6 backport,并将其作为基础,而不是编写完全自制的解决方案。然后当7出来时你可以放弃对backport的依赖并更改一些包名。
(完全披露:我们已经在生产中使用了LinkedTransferQueue一段时间了。没有问题)
答案 8 :(得分:0)
我必须说,上面描述的递归调用任务问题和等待结束子订单任务的解决方案并不能满足我。我的解决方案受到来自Oracle的原始文档的启发:CountDownLatch以及示例:Human resources CountDownLatch。
类HRManagerCompact实例中进程中的第一个常见线程有两个子线程的等待锁存器,它们有后续2个子线程的等待锁存器......等等。
当然,latch可以设置在不同于2的值(在CountDownLatch的构造函数中),以及可以在迭代中建立可运行对象的数量,即ArrayList,但它必须对应(计数次数必须是等于CountDownLatch构造函数中的参数。)
注意,根据限制条件,锁存器的数量呈指数增长: 'level.get()&lt; 2',以及对象的数量。 1,2,4,8,16 ......和锁存器0,1,2,4 ......如您所见,对于四个级别(level.get()&lt; 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();
}
}
可能的评论输出(有一定概率):
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
答案 9 :(得分:0)
我能想到的唯一不合理的解决方案是直接使用ThreadPoolExecutor并偶尔查询其getPoolSize()。这样做真的没有更好的方法吗?
您必须按正确的顺序使用shutdown() ,
awaitTermination()and shutdownNow()
方法。
shutdown()
:启动有序关闭,其中先前提交的任务已执行,但不会接受任何新任务。
awaitTermination()
:阻塞,直到所有任务在关闭请求或超时发生后完成执行,或者当前线程被中断,以先发生者为准。
shutdownNow()
:尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行的任务列表。
来自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();
}
如果条件是条件,则可以在完成任务的情况下长时间替换条件,如下所示:
更改
if (!pool.awaitTermination(60, TimeUnit.SECONDS))
要
while(!pool.awaitTermination(60, TimeUnit.SECONDS)) {
Thread.sleep(60000);
}
您可以参考其他替代方案(join()
除外,它可以与独立线程一起使用):
答案 10 :(得分:0)
您可以使用运行程序来跟踪正在运行的线程:
<=>
要使用它,您只需添加一个依赖项:
编译'com.github.matejtymes:javafixes:1.3.0'