fix(ticket): TicketScanService 锁内复制 key/value,修 fastutil entry 复用导致主线程 NPE 崩服
根因:上一版(scope-perthread-lock)锁内只快照 entry 引用,锁外读 entry.getValue()。 fastutil Long2ObjectOpenHashMap.long2ObjectEntrySet() 的 entry 对象是可复用的—— 锁内遍历时 snapshot 列表所有元素最终指向同一 entry 实例,解锁后 DR 维度线程 (经 DistanceManagerLock) 增删 ticket 会改写/清空该 entry,导致 getValue() 返回 null。 实测崩服:crash-2026-10-13_18.36,Server thread NPE "Cannot invoke SortedArraySet.isEmpty() because tickets is null" at TicketScanService.visitEntry:62。 修复:锁内遍历时立即把 getLongKey()/getValue() 复制成不可变 record EntrySnapshot, 存进列表;锁外遍历 Snapshot。value 为 null 时跳过。下游 visitEntry 加 tickets==null 兜底。 DR 未加载的无锁路径行为不变。
This commit is contained in:
1 parent
3fbc956a62
commit
9c309bfed0
1 file changed
+26
-11
@@ -16,13 +16,21 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
* <b>并发安全</b>:{@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 <b>复制</b>出来再遍历,
|
||||
* 避免持锁期间执行 visitor(可能较慢)。
|
||||
* <p>
|
||||
* <b>注意</b>:{@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<Ticket<?>> 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<Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>>> snapshot;
|
||||
// DR 共存:锁内复制 key/value 快照,锁外遍历,与维度线程的 ticket 写互斥。
|
||||
// 必须复制 value,不能存 entry 引用——fastutil entry 可复用,锁外读会拿到 null/错值。
|
||||
List<EntrySnapshot> snapshot;
|
||||
lock.lock();
|
||||
try {
|
||||
snapshot = new ArrayList<>();
|
||||
for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> entry :
|
||||
distanceManager.getTickets().long2ObjectEntrySet()) {
|
||||
snapshot.add(entry);
|
||||
SortedArraySet<Ticket<?>> value = entry.getValue();
|
||||
if (value != null) {
|
||||
snapshot.add(new EntrySnapshot(entry.getLongKey(), value));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> entry : snapshot) {
|
||||
visitEntry(entry, visitor);
|
||||
for (EntrySnapshot entry : snapshot) {
|
||||
visitEntry(entry.chunkPos(), entry.tickets(), visitor);
|
||||
}
|
||||
}
|
||||
|
||||
private static void visitEntry(Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> entry,
|
||||
TicketChunkVisitor visitor) {
|
||||
long chunkPos = entry.getLongKey();
|
||||
SortedArraySet<Ticket<?>> tickets = entry.getValue();
|
||||
if (tickets.isEmpty()) {
|
||||
visitEntry(entry.getLongKey(), entry.getValue(), visitor);
|
||||
}
|
||||
|
||||
private static void visitEntry(long chunkPos, SortedArraySet<Ticket<?>> 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<Ticket<?>> tickets);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user