如何在Spring TaskExecutor框架中管理线程

时间:2017-03-10 09:19:03

标签: java spring multithreading blockingqueue

我有一个BlockingQueue Runnable - 我可以使用TaskExecutor个实现之一执行所有任务,并且所有任务都将并行运行。 但是有些Runnable依赖于其他人,这意味着他们需要在Runnable完成时等待,然后才能执行。

规则很简单:每个Runnable都有一个代码。具有相同代码的两个Runnable不能同时运行,但如果代码不同,则它们应该并行运行。 换句话说,所有正在运行的Runnable都需要有不同的代码,所有"重复"应该等。

问题是线程结束时没有事件/方法/任何东西。 我可以在每个Runnable中构建这样的通知,但我不喜欢这种方法,因为它将在线程结束之前完成,而不是在它结束之后完成

java.util.concurrent.ThreadPoolExecutor有方法afterExecute,但需要实现 - Spring只使用默认实现,并忽略此方法。

即使我这样做,也会变得复杂,因为我需要追踪两个额外的集合:Runnable已经执行(没有实现可以访问这些信息)以及那些被推迟的因为它们有重复的代码。

我喜欢BlockingQueue方法,因为没有轮询,线程只是在队列中有新内容时激活。但也许有更好的方法来管理Runnable之间的这种依赖关系,所以我应该放弃BlockingQueue并使用不同的策略?

2 个答案:

答案 0 :(得分:1)

如果不同代码的数量不是那么大,那么由BarrySW19提供的每个可能代码的单独线程执行器的方法就可以了。 如果整个线程数变得不可接受,那么,我们可以使用一个actor(来自Akka或其他类似的库)而不是单个线程执行器:

public class WorkerActor extends UntypedActor {
  public void onReceive(Object message) {
    if (message instanceof Runnable) {
      Runnable work = (Runnable) message;
      work.run();
    } else {
      // report an error
    }
  }
}

与原始解决方案一样,ActorRef的{​​{1}} s在HashMap中收集。获取(检索或创建)与给定代码对应的WorkerActor后,ActorRef workerActorRef将提交给Runnable job执行。

如果您不想对actor库有依赖关系,可以从头开始编写workerActorRef.tell(job)

WorkerActor

当获取(检索或创建)与给定代码对应的public class WorkerActor implements Runnable, Executor { Executor executor=ForkJoinPool.commonPool(); // or can by assigned in constructor LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueu<>(); boolean running = false; public synchronized void execute(Runnable job) { queue.put(job); if (!running) { executor.execute(this); // execute this worker, not job! running=true; } public void run() { for (;;) { Runnable work=null; synchronized (this) { work = queue.poll(); if (work==null) { running = false; return; } } work.run(); } } } 时,WorkerActor worker将提交给Runnable job执行。

答案 1 :(得分:0)

我们想到的另一种策略是为每个可能的代码分别设置单个线程执行程序。然后,当您想要提交新的Runnable时,您只需查找正确的执行程序以用于其代码并提交作业。

根据您拥有的代码数量,这可能会或可能不是一个好的解决方案。要考虑的主要问题是运行的并发线程数可能与您拥有的不同代码的数量一样高。如果您有许多不同的代码,这可能是个问题。

当然,您可以使用Semaphore来限制并发运行的作业数量;你仍然会为每个代码创建一个线程,但实际上只有一个有限的数字可以同时执行。例如,这将按代码序列化作业,允许最多三个不同的代码同时运行:

public class MultiPoolExecutor {
    private final Semaphore semaphore = new Semaphore(3);

    private final ConcurrentMap<String, ExecutorService> serviceMap 
            = new ConcurrentHashMap<>();

    public void submit(String code, Runnable job) {
        ExecutorService executorService = serviceMap.computeIfAbsent(
                code, (k) -> Executors.newSingleThreadExecutor());
        executorService.submit(() -> {
            semaphore.acquireUninterruptibly();
            try {
                job.run();
            } finally {
                semaphore.release();
            }
        });
    }
}

另一种方法是修改Runnable以释放锁并检查可以在完成时运行的作业(因此避免轮询) - 类似这样的示例,它将所有作业保留在列表中,直到它们为止可以提交。布尔锁存器确保每个代码只有一个作业在任何时候都被提交给线程池。每当新作业到达或正在运行的作业完成时,代码会再次检查可以提交的新作业(CodedRunnable只是具有代码属性的Runnable的扩展名。)

public class SubmissionService {
    private final ExecutorService executorService = Executors.newFixedThreadPool(5);
    private final ConcurrentMap<String, AtomicBoolean> locks = new ConcurrentHashMap<>();
    private final List<CodedRunnable> jobs = new ArrayList<>();

    public void submit(CodedRunnable codedRunnable) {
        synchronized (jobs) {
            jobs.add(codedRunnable);
        }
        submitWaitingJobs();
    }

    private void submitWaitingJobs() {
        synchronized (jobs) {
            for(Iterator<CodedRunnable> iter = jobs.iterator(); iter.hasNext(); ) {
                CodedRunnable nextJob = iter.next();
                AtomicBoolean latch = locks.computeIfAbsent(
                        nextJob.getCode(), (k) -> new AtomicBoolean(false));
                if(latch.compareAndSet(false, true)) {
                    iter.remove();
                    executorService.submit(() -> {
                        try {
                            nextJob.run();
                        } finally {
                            latch.set(false);
                            submitWaitingJobs();
                        }
                    });
                }
            }
        }
    }
}

这种方法的缺点是代码需要在每个任务完成后扫描整个等待作业列表。当然,您可以提高效率 - 完成任务实际上只需要检查具有相同代码的其他作业,因此可以将作业存储在Map<String, List<Runnable>>结构中,以便更快地处理。