简体   繁体   English

在while循环中轮询阻塞队列时安全线程

[英]Thread safely while polling a blocking queue in a while loop

Consider the below example where an ItemDeletionManager can enqueue items to delete.考虑下面的示例,其中ItemDeletionManager可以将要删除的项目排入ItemDeletionManager A worker thread can call it's createJob() method that will create a compound job to delete all items at once.工作线程可以调用它的createJob()方法,该方法将创建一个复合作业以一次删除所有项目。 Assume that the worker thread periodically runs every few minutes or so.假设工作线程每隔几分钟定期运行一次。 But items to delete can get enqueued every few seconds.但是要删除的项目每隔几秒钟就会被排队。 This is why it's necessary to create a compound job and not have the worker create one job for one item on each iteration.这就是为什么有必要创建一个复合作业而不是让工人在每次迭代中为一个项目创建一个作业。

If we now have two workers that share the same manager like below,如果我们现在有两个工人共享同一个经理,如下所示,

ItemDeletionManager itemManager = new ItemDeletionManager();

DeletionWorker a = new DeletionWorker(itemManager);
DeletionWorker b = new DeletionWorker(itemManager);

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
scheduledExecutorService.scheduleAtFixedRate(a, 0, 10, TimeUnit.MINUTES);
scheduledExecutorService.scheduleAtFixedRate(b, 0, 10, TimeUnit.MINUTES);

I could end up in a situation where there is exactly one item to delete, and both workers have validated that the blocking queue isn't empty.我最终可能会遇到一种情况,即只有一个项目要删除,并且两个工作人员都已验证阻塞队列不为空。 But after one thread polls for the item to delete and creats it's deletion assignment, the second worker gets a null and ends up creating a deletion assignment of null.但是在一个线程轮询要删除的项目并创建它的删除分配之后,第二个工作人员获得一个null并最终创建一个空值的删除分配。

The easiest option would be to synchronize ItemDeletionManager 's create job.最简单的选择是synchronize ItemDeletionManager的创建作业。 But is there a better solution?但是有更好的解决方案吗?


/**
 * Worker that executes ItemDeletionManager's create job.
 */
class DeletionWorker implements Runnable {

    ItemDeletionManager itemManager;

    public DeletionWorker(ItemDeletionManager itemManager) {
        this.itemManager = itemManager;
    }

    @Override
    public void run() {
        itemManager.createJob();
    }
}

/**
 * Manages deletion of Items.
 */
class ItemDeletionManager {

    private final Queue<Integer> idsOfItemsToDelete;

    public ItemDeletionManager() {
        this.idsOfItemsToDelete = new LinkedBlockingQueue<>();
    }

    public void enqueue(int itemId) {
        this.idsOfItemsToDelete.add(itemId);
    }

    public CompoundJob createJob() {
        CompoundJob job = new CompoundJob();
        while (!idsOfItemsToDelete.isEmpty()) {
            // 2 threads can reach this point at the same time
            Integer itemId = idsOfItemsToDelete.poll();
            job.addSubAssignment(DeletionAssignment.of(itemId));
        }
        return job;
    }
}

/**
 * Implementers of this handle its respective requirement.
 * Eg: DeletionAssignment handles deletion of item, CreationAssignment handles creation of item and so on..
 */
interface Assignment {

    void execute();
}

/**
 * Executes a collection of assignments.
 */
class CompoundJob {

    private final List<Assignment> subAssignments = new ArrayList<>();

    public void addSubAssignment(Assignment assignment) {
        subAssignments.add(assignment);
    }

    public void doJob() {
        for (Assignment assignment : subAssignments) {
            assignment.execute();
        }
    }
}

As you have well identified, calling isEmpty() and then poll() isn't thread safe unless you synchronize, but it's quite sad to do so with a concurrent datastructure.正如您所确定的那样,除非您同步,否则调用 isEmpty() 然后 poll() 不是线程安全的,但是使用并发数据结构这样做非常令人难过。

In fact you shouldn't call isEmpty() beforehand.事实上,您不应该事先调用 isEmpty()。 Whether you call take() and it blocks until there's an item, or you call poll() directly without checking isEmpty() first and know that, if you get null, then it means that the queue was empty.无论你调用 take() 并阻塞直到有一个项目,或者你直接调用 poll() 而不先检查 isEmpty() 并且知道,如果你得到 null,那么这意味着队列是空的。 You also have the intermediate option between take() and poll() by specifying a time limit.您还可以通过指定时间限制来选择 take() 和 poll() 之间的中间选项。

I suggest you to use poll() directly and handle null, like so:我建议您直接使用 poll() 并处理 null,如下所示:

while (true) {
  Integer itemId = idsOfItemsToDelete.poll();
  if (itemId==null) break;
  job.addSubAssignment(DeletionAssignment.of(itemId));
}

Even if several threads execute this same code concurrently, you are guaranteed to not get twice the same item.即使多个线程同时执行相同的代码,您也可以保证不会获得两次相同的项目。 However, retrieval won't be equally distributed, ie it may happen that one of the thread take all items and the other take none.但是,检索不会平均分配,即可能发生一个线程获取所有项目而另一个不获取任何项目的情况。

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

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM