使用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()+"}";
}
}
答案 0 :(得分:0)
正如@BenManes在评论中指出的那样,问题在于使用了比较器。当比较器的结果为0时,即使两个任务不相等,也会覆盖条目。实际上,比较者应该考虑与hashCode
和equals
相同的字段。
使用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进行排序。
对于用例,具有相同执行时间的任务顺序无关紧要。