Telegram单词学习Bot遭遇ConcurrentModificationException问题求助
问题分析与解决方案
核心原因定位
java.util.ConcurrentModificationException 本质是集合在遍历过程中被并发修改,结合你Telegram Bot的场景(首次添加正常、再次触发/start报错,异常来自线程池线程pool-1-thread-1),大概率是两类问题:
- SQLite连接管理类存在线程不安全的共享集合/状态,被Bot多线程请求同时操作;
- Bot主逻辑中存在未做线程安全保护的共享数据结构,在Dispatcher线程池处理请求时引发冲突。
针对性排查与修复
1. 检查SQLite连接管理的线程安全性
SQLite是文件型数据库,默认连接不支持多线程复用,若你的连接管理类存在以下情况,直接触发异常:
- 多个线程共享同一个
Connection实例; - 用非线程安全集合(比如
ArrayList)管理连接池,且未加锁保护遍历/修改操作; - 关闭连接时未正确从集合中移除,导致遍历连接池时出现无效条目。
修复建议:
- 彻底放弃复用
Connection实例,每个请求单独创建并关闭(确认你的实现真的是每次新建,而非复用了某个共享连接); - 若用连接池,必须使用线程安全集合(比如
CopyOnWriteArrayList),或对集合的所有操作加synchronized锁; - 开启SQLite的WAL模式提升并发安全性:在连接URL后追加
?journal_mode=WAL。
2. 检查Bot主逻辑的线程安全
Telegram Bot的Dispatcher通过线程池处理请求,若Bot类中存在共享的非线程安全对象(比如临时存储用户输入的List/Map),两次/start请求并发触发时会引发遍历修改冲突。
修复建议:
- 所有共享数据结构替换为线程安全实现:用
ConcurrentHashMap替代HashMap,CopyOnWriteArrayList替代ArrayList; - 对共享对象的读写操作加锁:如果必须用非线程安全集合,在遍历、添加、删除操作外包裹
synchronized块; - 避免在Bot成员变量中存储用户临时状态,改用Telegram Bot API提供的
userData或直接存入数据库。
3. 结合异常堆栈精准定位
从异常堆栈中找到最顶部的业务代码栈帧:
- 如果堆栈指向连接管理类的集合遍历方法,直接针对连接池的线程安全改造;
- 如果指向Bot命令处理中的集合操作,就修复业务逻辑的线程安全问题。
代码示例参考
线程安全的SQLite连接管理(简易版)
public class SQLiteConnectionManager { private static final String DB_URL = "jdbc:sqlite:words.db?journal_mode=WAL"; private static final CopyOnWriteArrayList<Connection> connectionPool = new CopyOnWriteArrayList<>(); private static final int MAX_POOL_SIZE = 5; public static Connection getConnection() throws SQLException { // 遍历可用连接(线程安全集合支持并发遍历) for (Connection conn : connectionPool) { if (!conn.isClosed()) { return conn; } } // 无可用则创建新连接 if (connectionPool.size() < MAX_POOL_SIZE) { Connection newConn = DriverManager.getConnection(DB_URL); connectionPool.add(newConn); return newConn; } throw new SQLException("连接池已满"); } public static void closeConnection(Connection conn) { if (conn != null) { try { conn.close(); connectionPool.remove(conn); } catch (SQLException e) { e.printStackTrace(); } } } }
Bot命令处理的线程安全改造
public class WordBot extends TelegramLongPollingBot { // 用线程安全集合存储用户临时输入状态 private final ConcurrentHashMap<Long, String> userPendingOps = new ConcurrentHashMap<>(); @Override public void onUpdateReceived(Update update) { if (update.hasMessage() && update.getMessage().hasText()) { String text = update.getMessage().getText(); long chatId = update.getMessage().getChatId(); if (text.equals("/start")) { userPendingOps.put(chatId, "waiting_for_word"); sendMessage(chatId, "请输入要添加的单词:"); } else if ("waiting_for_word".equals(userPendingOps.get(chatId))) { // 存储单词到数据库,用try-with-resources自动关闭连接 try (Connection conn = SQLiteConnectionManager.getConnection()) { String sql = "INSERT INTO words (word, chat_id) VALUES (?, ?)"; try (PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, text); pstmt.setLong(2, chatId); pstmt.executeUpdate(); sendMessage(chatId, "单词添加成功!"); userPendingOps.remove(chatId); } } catch (SQLException e) { e.printStackTrace(); sendMessage(chatId, "添加失败,请重试"); } } } } // 封装发送消息方法 private void sendMessage(long chatId, String text) { SendMessage message = new SendMessage(); message.setChatId(chatId); message.setText(text); try { execute(message); } catch (TelegramApiException e) { e.printStackTrace(); } } // 实现Bot的其他必填方法... }
额外注意事项
- 不要在Bot成员变量中持有
Connection实例,必须每次使用都从连接池获取或新建; - 控制Dispatcher线程池的线程数量,SQLite对并发写入的支持有限,建议线程数不超过5;
- 若使用ORM框架操作数据库,确保框架本身做了线程安全处理。
内容的提问来源于stack exchange,提问作者Ivan Ivanov
相关产品推荐
相关产品推荐

