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