具有唯一任务的线程池队列

时间:2015-03-31 12:50:21

标签: java multithreading spring java.util.concurrent

我正在使用ThreadPoolTaskExecutor(春天)来异步执行某些任务。

所需任务将从外部DB加载一些对象到我的系统内存中。 我使用的最大线程池大小为10,最大队列大小为100。

假设所有10个线程都被占用从我的数据库中获取对象并创建了一个任务,它将进入队列。现在创建了另一个任务,它应该从DB获取相同的对象(DB中的相同键),它也将进入队列(假设所有10个线程仍然被占用)。

因此,我的队列可能会很容易地完成重复任务,这些任务将依次执行,我不希望这种情况发生。

我认为解决方案应该以独特集合的形式出现,该集合充当线程池队列。 在引擎盖下ThreadPoolTask​​Executor使用LinkedBlockingQueue,它不提供唯一性。

我想到了一些可能的解决方案,但没有人满足我:

  • 使用ThreadPoolExecutor代替ThreadPoolTask​​Executor。 ThreadPoolExecutor提供了一个构造函数,它允许我确定线程池队列类型,但它需要实现BlockingQueue接口。我找不到保持唯一性的实现。

这导致我尝试扩展LinkedBlockingQueue并覆盖add:

public boolean add(E e)
    if(!this.contains(e)) {
        return super.add(e);
    } else {
        return false;
    }
}

但据我所知,由于contains方法受到O(n)的限制,这会导致性能大幅下降 - 这是个坏主意。

什么可以解决我的问题?我的目标是获得良好的性能(在内存性能权衡的情况下,我不介意为性能放弃内存)。

3 个答案:

答案 0 :(得分:6)

使用GuavaListenableFuture您可以做类似的事情(尚未测试)

Set<String> uniqueQueue = Sets.newConcurrentHashSet();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS, Queues.newLinkedBlockingQueue(100));
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(threadPoolExecutor);

String t1 = "abc";
if(uniqueQueue.add(t1)) {
    ListenableFuture<String> future = executorService.submit(() -> "do something with " + t1);
    Futures.addCallback(future, new FutureCallback<String>() {
        @Override
        public void onSuccess(String result) {
            uniqueQueue.remove(t1);
        }

        @Override
        public void onFailure(Throwable t) {
            uniqueQueue.remove(t1);
        }
    });
}

导致

  • 只有当前未处理或队列中的项目才会添加到队列中(uniqueQueue
  • 已处理的项目将从uniqueQueue
  • 中删除
  • 队列中最多只有100个项目

此实现无法处理

    {li> Exceptionssubmit()方法引发
  • unqiueQueue
  • 中的最大项目数

参考您将数据库中的对象加载到内存中的要求,您可能需要查看Guava's Caches

<强>更新

答案 1 :(得分:1)

类似于公认解决方案的解决方案,但基于Spring(与Guava相对):

创建界面 RunnableWithId

 public interface RunnableWithId extends Runnable {

    /**
     * @return A unique id for this task
     */
    String getTaskId();
}

创建另一个界面 TaskWithIdExecutor

import org.springframework.core.task.TaskExecutor;


public interface TaskWithIdExecutor extends TaskExecutor {

    /**
     * Executes the given task if it is not queued or already running
     *
     * @param task The task to execute
     */
    void executeIfNotQueuedOrRunningAlready(RunnableWithId task);
}

创建自定义执行者 UniquTaskExecutor

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;

import java.util.Set;

/**
 * In addition to all the abilities of ThreadPoolTaskExecutor adds the ability
 * to execute a task only if it is not already running/queued using the
 * executeIfNotQueuedOrRunningAlready method.
 *
 * @see ThreadPoolTaskExecutor
 */
public class UniquTaskExecutor extends ThreadPoolTaskExecutor implements TaskWithIdExecutor {

    private Set<String> queuedTasks;

    public UniquTaskExecutor() {
        queuedTasks = Sets.newConcurrentHashSet();
    }

    @Override
    public void execute(Runnable task) {
        super.execute(task);
    }

    /**
     * @param task The task to execute
     */
    @Override
    public void executeIfNotQueuedOrRunningAlready(RunnableWithId task) {
        if (queuedTasks.add(task.getTaskId())) {
            ListenableFuture<?> res = submitListenable(task);
            res.addCallback(new ListenableFutureCallback<Object>() {
                @Override
                public void onFailure(Throwable throwable) {
                    queuedTasks.remove(task.getTaskId());
                }

                @Override
                public void onSuccess(Object o) {
                    queuedTasks.remove(task.getTaskId());
                }
            });
        }
    }
}

使用 UniquTaskExecutor executeIfNotQueuedOrRunningAlready 方法来实现任务执行的唯一性。

答案 2 :(得分:0)

如果您被允许管理数据库,我建议使用数据库本身来防止重复工作:

  • 向表格添加lockid列
  • 在表格中添加状态栏(可能是“新”和“已完成”)
  • 确保您的数据库隔离级别至少为READ_COMMITTED

然后在主线程中尝试这样的事情:

Random rand = new Random();
int lockId = rand.nextInt(Integer.MAX_VALUE - 1) + 1;
String update = "UPDATE DB.Table SET lockid=" + lockId + " WHERE lockid=0 AND status='new' " // + AND your conditions + LIMIT ##
String select = "SELECT * FROM DB.Table WHERE lockid=" + lockId;
// now execute those sql statements with QueryRunner or whatever you use in-house

从select返回的行是您添加到队列的行。

然后,您有一个实现Runnable的类,通过从队列中检索这些行来处理这些行。处理完一行后,再执行一次SQL更新(在Runnable中)将lockId设置为零,并将状态设置为“done”。

即使你有多台机器都有自己的队列,这样做也有效。