[英]Implications of weakly consistent ConcurrentSkipListSet
Using a ConcurrentSkipListSet
I have observed some wired behaviour, that I suspect is caused by the weakly consistency of the concurrent set. 通过使用
ConcurrentSkipListSet
我观察到一些有线行为,我怀疑这是由并发集的弱一致性引起的。
The JavaDoc has this to say on that topic: JavaDoc在该主题上有这样的说法:
Most concurrent Collection implementations (including most Queues) also differ from the usual java.util conventions in that their Iterators and Spliterators provide weakly consistent rather than fast-fail traversal:
大多数并发的Collection实现(包括大多数Queue)也与通常的java.util约定不同,它们的迭代器和拆分器提供了弱一致性而不是快速失败遍历:
- they may proceed concurrently with other operations
他们可能会与其他操作同时进行
- they will never throw ConcurrentModificationException
他们永远不会抛出ConcurrentModificationException
- they are guaranteed to traverse elements as they existed upon construction exactly once, and may (but are not guaranteed to) reflect any modifications subsequent to construction.
它们被保证可以遍历在构造时已经存在的元素一次,并且可以(但不保证)反映出构造后的任何修改。
This is the code that I use: 这是我使用的代码:
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());
}
At occasion I can see log output like this: 有时我会看到这样的日志输出:
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
These are two almost consecutive log lines. 这是两条几乎连续的日志行。 Please note that they are executed on different threads.
请注意,它们在不同的线程上执行。 The
TimedTask
entry that is added is not listed in the second log line. 第二个日志行中未列出添加的
TimedTask
条目。
Am I correct in my assumption that this is due to the weakly consistency? 我是否认为这是由于弱一致性导致的? If so, would this also imply that the
iterator.next()
retrieves a different entry than iterator.remove()
deletes? 如果是这样,这是否也意味着
iterator.next()
检索的条目与iterator.remove()
删除的条目不同?
What I am observing, is that this added entry is never processed and does not show up in the concurrent set at any time. 我观察到的是,此添加的条目从未被处理过,并且在任何时候都不会显示在并发集中。
What would be a good solution to avoid this? 有什么好的方法可以避免这种情况? What comes to my mind, is create a copy of the set and iterate over that one, as it is acceptable, that entries can be processed in a future iteration, as long as they are processed.
我想到的是创建该集合的副本,并在该集合上进行迭代,这是可以接受的,只要条目可以处理,就可以在以后的迭代中对其进行处理。 Looking at Weakly consistent iterator by ConcurrentHashMap suggests the iteration already happens on a copy of the set, so this might not change anything.
查看ConcurrentHashMap的弱一致性迭代器,表明迭代已经在集合的副本上进行,因此这可能不会有任何改变。
EDIT Sample implementation of a TimedTask
: 编辑
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()+"}";
}
}
As @BenManes pointed out in his comment, the issue is with the Comparator used. 正如@BenManes在其评论中指出的那样,问题出在所使用的比较器。 When the result of the Comparator is 0, even through the two tasks are not equal, entries will be overridden.
当比较器的结果为0时,即使通过两个任务不相等,条目也将被覆盖。 In effect, the Comparator should consider the same fields as
hashCode
and equals
. 实际上,比较器应考虑与
hashCode
和equals
相同的字段。 Use a Comparator
implementation like this: 使用这样的
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;
}
With an implementation like this the comparison is based on the execution time and when both of them are the same (comparison is 0) ensure they are ordered based on their UUID. 对于这样的实现,比较基于执行时间,并且当两者相同时(比较为0),请确保根据其UUID对其进行排序。
For the use case the order of tasks with the same execution time is not relevant. 对于用例,具有相同执行时间的任务顺序无关紧要。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.