You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程与Socket传输:共享ArrayList<String>的问题排查与解决

排查与解决多线程共享ArrayList发送异常/数据不一致问题

嗨,这个场景我在做多人在线聊天服务器时踩过坑,咱们一步步来梳理排查思路和解决方案:

一、先排查问题根源

1. 定位异常类型

先盯紧控制台的异常堆栈信息:

  • 如果是ConcurrentModificationException:这是ArrayList的经典坑——它是非线程安全的,当一个线程在遍历/序列化它的时候,另一个线程同时执行add/remove操作,就会触发这个异常。
  • 如果是序列化相关异常(比如NotSerializableException):虽然String是可序列化的,但要检查是不是Socket输出流提前关闭,或者ObjectOutputStream初始化有问题。
  • 如果没有异常但数据不一致:大概率是多线程读写没做同步,导致发送的列表是“过时”的快照。

2. 检查操作时机的冲突

在add/remove用户名和发送列表的代码处加日志,打印线程ID、操作类型和当前列表内容,直观排查冲突:

// 添加用户名时
System.out.println(String.format("线程%s:添加用户%s,当前列表:%s", Thread.currentThread().getId(), username, userList));
// 发送列表时
System.out.println(String.format("线程%s:发送列表,当前列表:%s", Thread.currentThread().getId(), userList));

通过日志能快速确认是不是发送操作和修改操作同时发生,导致数据异常。

3. 验证序列化过程

如果怀疑是序列化出问题,先本地测试序列化逻辑:

// 序列化到本地文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.dat"))) {
    oos.writeObject(userList);
}
// 反序列化验证
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.dat"))) {
    List<String> testList = (List<String>) ois.readObject();
    System.out.println(testList);
}

如果本地序列化没问题,那问题大概率出在Socket传输或多线程同步上。

二、针对性解决方法

1. 替换为线程安全集合(推荐)

用CopyOnWriteArrayList<String>替代普通ArrayList,它的核心逻辑是修改时复制新数组,遍历/读操作使用旧数组,天然避免ConcurrentModificationException,而且不需要手动加锁,非常适合这种“读多写少”的场景(客户端加入退出频率远低于列表查询/发送频率):

private CopyOnWriteArrayList<String> userList = new CopyOnWriteArrayList<>();

// 客户端加入
userList.add(username);
// 客户端退出
userList.remove(username);
// 发送列表
oos.writeObject(userList);

2. 手动加同步锁(兼容旧代码)

如果不想替换集合,就在所有操作userList的地方(add、remove、发送前的快照复制)加上同一个锁对象,确保同一时间只有一个线程操作列表:

private List<String> userList = new ArrayList<>();
private final Object lock = new Object(); // 用专属锁对象,别直接用userList当锁

// 添加用户名
synchronized (lock) {
    userList.add(username);
}

// 移除用户名
synchronized (lock) {
    userList.remove(username);
}

// 发送列表(先复制快照,减少锁持有时间)
List<String> sendList;
synchronized (lock) {
    sendList = new ArrayList<>(userList); // 复制当前时刻的列表快照
}
oos.writeObject(sendList);

⚠️ 注意:别在锁块里执行Socket发送这种耗时操作,否则会导致其他线程等待锁的时间过长,拖慢服务器性能。复制快照后释放锁再发送才是最优解。

3. 避免遍历过程中修改

如果是自己循环遍历列表发送每个用户名(而非直接序列化整个列表),同样要确保遍历过程被同步保护,或者用CopyOnWriteArrayList——它的迭代器是弱一致性的,不会抛出ConcurrentModificationException。

内容的提问来源于stack exchange,提问作者Kerage Chan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 08:21:35