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