多线程与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
相关产品推荐
相关产品推荐

