弱一致的ConcurrentSkipListSet

时间:2018-05-19 13:09:21

标签: java concurrency iterator

使用ConcurrentSkipListSet我观察到一些有线行为,我怀疑这是由并发集的弱一致性引起的。

JavaDoc就此话题说了这个:

  

大多数并发Collection实现(包括大多数队列)   也有不同于通常的java.util约定   迭代器和分裂器提供弱一致而不是   快速失败遍历:

     
      
  • 他们可以与其他行动同时进行
  •   
  • 他们永远不会抛出ConcurrentModificationException
  •   
  • 它们可以保证遍历元素,因为它们在构造时只存在一次,并且可能(但不保证)反映   施工后的任何修改。
  •   

这是我使用的代码:

private final ConcurrentSkipListSet<TimedTask> sortedEvents;

public TimedUpdatableTaskList(){
    Comparator<TimedTask> comparator = 
        (task1, task2) -> task1.getExecutionTime().compareTo(task2.getExecutionTime());
    sortedEvents = new ConcurrentSkipListSet<>(comparator);
}

public void add(TimedTask task) {
    log.trace("Add task {}", task);
    sortedEvents.add(task);
}

public void handleClockTick(ClockTick event) {
    LocalDateTime now = date.getCurrentDate();
    logContent("Task list BEFORE daily processing ("+now+")");
    for (Iterator<TimedTask> iterator = sortedEvents.iterator(); iterator.hasNext();) {
        TimedTask task = iterator.next();
        Preconditions.checkNotNull(task.getExecutionTime(),
                "The exectution time of the task may not be null");
        if (task.getExecutionTime().isBefore(now)) {
            log.trace("BEFORE: Execute task {} scheduled for {} on {}",
                    task, task.getExecutionTime(), now);
            try {
                task.run();
                iterator.remove();
            } catch (Exception e) {
                log.error("Failed to execute timed task", e);
            }
            log.trace("AFTER: Execute task {} scheduled for {} on {}",
                    task, task.getExecutionTime(), now);
        }
        if (task.getExecutionTime().isAfter(now)) {
            break; // List is sorted
        }
    }
    logContent("Task list AFTER daily processing");
}

private void logContent(String prefix) {
    StringBuilder sb = new StringBuilder();
    sortedEvents.stream().forEach(task ->sb.append(task).append(" "));
    log.trace(prefix + ": "+sb.toString());
}

有时我可以看到这样的日志输出:

2018-05-19 13:46:00,453 [pool-3-thread-1] TRACE ... - Add task AIRefitTask{ship=Mercurius, scheduled for: 1350-07-16T08:45}
2018-05-19 13:46:00,505 [pool-3-thread-5] TRACE ... - Task list BEFORE daily processing (1350-07-16T09:45): AIRefitTask{ship=Tidewalker, scheduled for: 1350-07-16T08:45} AIRepairTask{ship=Hackepeter, scheduled for: 1350-07-16T13:45} ch.sahits.game.openpatrician.engine.event.task.WeaponConstructionTask@680da167 ch.sahits.game.openpatrician.engine.player.DailyPlayerUpdater@6e22f1ba AIRepairTask{ship=St. Bonivatius, scheduled for: 1350-07-17T03:45} AIRepairTask{ship=Hackepeter, scheduled for: 1350-07-17T05:45} ch.sahits.game.openpatrician.engine.event.task.WeeklyLoanerCheckTask@47571ace 

这是两条几乎连续的日志行。请注意,它们在不同的线程上执行。添加的TimedTask条目未在第二个日志行中列出。

我是否认为这是由于弱一致性造成的?如果是这样,这是否也意味着iterator.next()检索的条目与iterator.remove()删除的条目不同?

我所观察到的是,这个添加的条目永远不会被处理,并且不会在任何时候出现在并发集中。

避免这种情况会有什么好处?我想到的是,创建一个集合的副本并迭代该集合,因为可以接受的是,只要处理它们,就可以在将来的迭代中处理这些条目。查看Weakly consistent iterator by ConcurrentHashMap表明迭代已经发生在集合的副本上,因此这可能不会改变任何内容。

编辑 TimedTask的示例实现:

class AIRefitTask extends TimedTask {

    private static final Logger LOGGER = LogManager.getLogger(AIRefitTask.class);

    private AsyncEventBus clientServerEventBus;

    private ShipWeaponsLocationFactory shipWeaponLocationFactory;

    private ShipService shipService;

    private final IShip ship;
    private final EShipUpgrade level;
    private final IShipyard shipyard;

    public AIRefitTask(LocalDateTime executionTime, IShip ship, EShipUpgrade upgrade, IShipyard shipyard) {
        super();
        setExecutionTime(executionTime);
        LOGGER.debug("Add AIRefitTask for {} to be done at {}", ship.getName(), executionTime);
        this.ship = ship;
        this.level = upgrade;
        this.shipyard = shipyard;
    }

    @Override
    public void run() {
        EShipUpgrade currentLevel = ship.getShipUpgradeLevel();
        while (currentLevel != level) {
            ship.upgrade();
            List<IWeaponSlot> oldWeaponSlots = ship.getWeaponSlots();
            List<IWeaponSlot> newWeaponSlots = shipWeaponLocationFactory.getShipWeaponsLocation(ship.getShipType(), level);
            ship.setWeaponSlots(newWeaponSlots);
            for (IWeaponSlot slot : oldWeaponSlots) {
                if (slot.getWeapon().isPresent()) {
                    EWeapon weapon = (EWeapon) slot.getWeapon().get();
                    if (slot instanceof SecondaryLargeWeaponSlot) {
                        if (!shipService.isLargeWeapon(weapon)) { // ignore large weapons in secondary slots
                            shipService.placeWeapon(weapon, ship);
                        }
                    } else {
                        // Not secondary slot
                        shipService.placeWeapon(weapon, ship);
                    }
                }
            }
            currentLevel = ship.getShipUpgradeLevel();
        }
        ship.setAvailable(true);
        shipyard.removeCompletedUpgrade(ship);
        LOGGER.debug("Refited ship {}", ship.getName());
        clientServerEventBus.post(new RefitFinishedEvent(ship));
    }

    @Override
    public String toString() {
        return "AIRefitTask{ship="+ship.getUuid()+", scheduled for: "+getExecutionTime()+"}";
    }
}

1 个答案:

答案 0 :(得分:0)

正如@BenManes在评论中指出的那样,问题在于使用了比较器。当比较器的结果为0时,即使两个任务不相等,也会覆盖条目。实际上,比较者应该考虑与hashCodeequals相同的字段。 使用Comparator这样的实现:

public int compare(TimedTask task1, TimedTask task2) {
    int executionTimeBasedComparisonResult = task1.getExecutionTime().compareTo(task2.getExecutionTime());
    if (executionTimeBasedComparisonResult == 0) { // two execution times are equal
        return task1.getUuid().compareTo(task2.getUuid());
    }
    return executionTimeBasedComparisonResult;
}

通过这样的实现,比较基于执行时间,并且当它们两者相同时(比较为0),确保它们根据其UUID进行排序。

对于用例,具有相同执行时间的任务顺序无关紧要。