diff --git a/src/main/java/com/moepus/serverwarashi/common/core/ticket/CrossModLocks.java b/src/main/java/com/moepus/serverwarashi/common/core/ticket/CrossModLocks.java new file mode 100644 index 0000000..3ed51b4 --- /dev/null +++ b/src/main/java/com/moepus/serverwarashi/common/core/ticket/CrossModLocks.java @@ -0,0 +1,60 @@ +package com.moepus.serverwarashi.common.core.ticket; + +import java.lang.reflect.Field; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 跨 mod 共享锁访问器。 + *

+ * DimensionalRipper 让维度 tick 跑在独立线程后,会并发修改 {@code DistanceManager.tickets} + * ({@code Long2ObjectOpenHashMap},非线程安全)。DR 用 + * {@code dev.modzuozhi.core.compat.DistanceManagerLock.LOCK} 串行化所有 ticket 写路径。 + *

+ * warashi 的性能采样在主线程遍历同一张 ticket 表({@link TicketScanService}),必须持同一把锁, + * 否则遍历会与维度线程的写并发 → fastutil fail-fast 迭代器 NPE(已实测)。 + *

+ * 软依赖:通过反射获取 DR 的锁;DR 未加载则 {@link #distanceManagerLock()} 返回 null, + * 调用方退化为无锁(原行为不变)。获取一次后缓存。 + */ +public final class CrossModLocks { + private static final String DR_LOCK_CLASS = + "dev.modzuozhi.core.compat.DistanceManagerLock"; + private static final String DR_LOCK_FIELD = "LOCK"; + + private static volatile ReentrantLock distanceManagerLock; + private static volatile boolean resolved; + + private CrossModLocks() { + } + + /** + * DR 的 DistanceManager 共享锁;DR 未加载时返回 null。 + * 首次调用解析后缓存(含「解析失败」结果,避免反复抛异常)。 + */ + public static ReentrantLock distanceManagerLock() { + if (!resolved) { + synchronized (CrossModLocks.class) { + if (!resolved) { + distanceManagerLock = resolve(); + resolved = true; + } + } + } + return distanceManagerLock; + } + + private static ReentrantLock resolve() { + try { + Class lockClass = Class.forName(DR_LOCK_CLASS, false, + Thread.currentThread().getContextClassLoader()); + Field field = lockClass.getField(DR_LOCK_FIELD); + Object value = field.get(null); + if (value instanceof ReentrantLock lock) { + return lock; + } + } catch (Throwable ignored) { + // DR 未加载 / 字段缺失:退化为无锁。 + } + return null; + } +} \ No newline at end of file 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 eba4c35..e265fc6 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 @@ -6,8 +6,18 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.Ticket; import net.minecraft.util.SortedArraySet; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; + /** * 低分配遍历 DistanceManager 当前 ticket 表的访问器。 + *

+ * 并发安全:{@code DistanceManager.tickets} 是非线程安全的 + * {@code Long2ObjectOpenHashMap}。DimensionalRipper 维度线程会在 tick 中并发增删 ticket, + * 与主线程此处的遍历冲突 → fastutil fail-fast 迭代器 NPE(已实测)。 + * 故遍历时持 DR 的共享锁({@link CrossModLocks}),并在锁内把 entry 快照到局部 List 再遍历, + * 避免持锁期间执行 visitor(可能较慢)。DR 未加载时锁为 null,退化为原无锁行为。 */ public final class TicketScanService { private TicketScanService() { @@ -19,15 +29,40 @@ public final class TicketScanService { } public static void visitTicketChunks(DistanceManagerAccessor distanceManager, TicketChunkVisitor visitor) { - for (Long2ObjectMap.Entry>> entry : - distanceManager.getTickets().long2ObjectEntrySet()) { - long chunkPos = entry.getLongKey(); - SortedArraySet> tickets = entry.getValue(); - if (tickets.isEmpty()) { - continue; + ReentrantLock lock = CrossModLocks.distanceManagerLock(); + if (lock == null) { + // DR 未加载:原行为,直接遍历。 + for (Long2ObjectMap.Entry>> entry : + distanceManager.getTickets().long2ObjectEntrySet()) { + visitEntry(entry, visitor); } - visitor.visitChunk(chunkPos, tickets); + return; } + // DR 共存:锁内快照 entry,锁外遍历,与维度线程的 ticket 写互斥。 + List>>> snapshot; + lock.lock(); + try { + snapshot = new ArrayList<>(); + for (Long2ObjectMap.Entry>> entry : + distanceManager.getTickets().long2ObjectEntrySet()) { + snapshot.add(entry); + } + } finally { + lock.unlock(); + } + for (Long2ObjectMap.Entry>> entry : snapshot) { + visitEntry(entry, visitor); + } + } + + private static void visitEntry(Long2ObjectMap.Entry>> entry, + TicketChunkVisitor visitor) { + long chunkPos = entry.getLongKey(); + SortedArraySet> tickets = entry.getValue(); + if (tickets.isEmpty()) { + return; + } + visitor.visitChunk(chunkPos, tickets); } @FunctionalInterface