diff --git a/src/main/java/com/moepus/serverwarashi/common/core/ticket/TicketScanService.java b/src/main/java/com/moepus/serverwarashi/common/core/ticket/TicketScanService.java index e265fc6..f52717d 100644 --- a/src/main/java/com/moepus/serverwarashi/common/core/ticket/TicketScanService.java +++ b/src/main/java/com/moepus/serverwarashi/common/core/ticket/TicketScanService.java @@ -16,13 +16,21 @@ import java.util.concurrent.locks.ReentrantLock; * 并发安全:{@code DistanceManager.tickets} 是非线程安全的 * {@code Long2ObjectOpenHashMap}。DimensionalRipper 维度线程会在 tick 中并发增删 ticket, * 与主线程此处的遍历冲突 → fastutil fail-fast 迭代器 NPE(已实测)。 - * 故遍历时持 DR 的共享锁({@link CrossModLocks}),并在锁内把 entry 快照到局部 List 再遍历, - * 避免持锁期间执行 visitor(可能较慢)。DR 未加载时锁为 null,退化为原无锁行为。 + * 故遍历时持 DR 的共享锁({@link CrossModLocks}),并在锁内把 key/value 复制出来再遍历, + * 避免持锁期间执行 visitor(可能较慢)。 + *

+ * 注意:{@code Long2ObjectOpenHashMap.long2ObjectEntrySet()} 返回的 entry 对象是可复用的, + * 锁外再读 {@code entry.getValue()} 可能已因并发写而被改写/清空(实测 null)。因此快照必须复制 + * key 与 value,不能只存 entry 引用。DR 未加载时锁为 null,退化为原无锁行为。 */ public final class TicketScanService { private TicketScanService() { } + /** 快照条目:chunkPos + tickets(锁内复制,与 map 解耦)。 */ + private record EntrySnapshot(long chunkPos, SortedArraySet> tickets) { + } + public static void visitTicketChunks(ServerLevel level, TicketChunkVisitor visitor) { DistanceManagerAccessor distanceManager = TicketUtils.getDistanceManager(level); visitTicketChunks(distanceManager, visitor); @@ -38,28 +46,35 @@ public final class TicketScanService { } return; } - // DR 共存:锁内快照 entry,锁外遍历,与维度线程的 ticket 写互斥。 - List>>> snapshot; + // DR 共存:锁内复制 key/value 快照,锁外遍历,与维度线程的 ticket 写互斥。 + // 必须复制 value,不能存 entry 引用——fastutil entry 可复用,锁外读会拿到 null/错值。 + List snapshot; lock.lock(); try { snapshot = new ArrayList<>(); for (Long2ObjectMap.Entry>> entry : distanceManager.getTickets().long2ObjectEntrySet()) { - snapshot.add(entry); + SortedArraySet> value = entry.getValue(); + if (value != null) { + snapshot.add(new EntrySnapshot(entry.getLongKey(), value)); + } } } finally { lock.unlock(); } - for (Long2ObjectMap.Entry>> entry : snapshot) { - visitEntry(entry, visitor); + for (EntrySnapshot entry : snapshot) { + visitEntry(entry.chunkPos(), entry.tickets(), visitor); } } private static void visitEntry(Long2ObjectMap.Entry>> entry, TicketChunkVisitor visitor) { - long chunkPos = entry.getLongKey(); - SortedArraySet> tickets = entry.getValue(); - if (tickets.isEmpty()) { + visitEntry(entry.getLongKey(), entry.getValue(), visitor); + } + + private static void visitEntry(long chunkPos, SortedArraySet> tickets, + TicketChunkVisitor visitor) { + if (tickets == null || tickets.isEmpty()) { return; } visitor.visitChunk(chunkPos, tickets); @@ -69,4 +84,4 @@ public final class TicketScanService { public interface TicketChunkVisitor { void visitChunk(long chunkPos, SortedArraySet> tickets); } -} +} \ No newline at end of file