Java:ArrayList损坏?REST服务异常排查求助
这问题挺典型的,咱们从你遇到的两个异常现象入手,一步步揪出根源:
1. 调试时的com.sun.jdi.InvocationException
你猜的没错,这个异常确实和toString()方法有关。Eclipse调试器在悬停查看对象时,会自动调用对象的toString()方法生成显示文本。如果你的User类的toString()方法存在以下问题,就会触发这个异常:
- 引用了null字段(比如
return "User{" + "username='" + username + '\'' + ", role=" + role.getName() + '}',如果role为null就会抛NPE); - 存在循环引用(比如
User里有Role对象,Role里又引用了User,调用toString()时会无限递归); - 在多线程环境下访问了未同步的状态字段,导致调用
toString()时对象处于不一致的状态。
2. ArrayList的size为-1
这是最关键的线索——正常情况下ArrayList的size不可能是负数。ArrayList的内部size变量是int类型,初始值为0,只有add/remove等方法会修改它,而且这些方法里都会保证size的合法性。出现size=-1的唯一原因是:多线程并发修改破坏了ArrayList的内部结构。
看你的代码:线程安全隐患是核心问题
你的OnlineUserTracker类里用了普通的ArrayList,但REST服务是多线程环境——多个请求会同时调用checkAuthorized(执行add)和logoff(执行remove)方法,而ArrayList本身是非线程安全的。
当多个线程同时操作同一个ArrayList时,会发生数据竞争:
- 比如两个线程同时执行
add,可能导致size变量被重复累加或者出现异常值; - 执行
remove时的遍历操作(getUserFromUsername里的for循环)和修改操作同时进行,会破坏ArrayList的内部数组结构; - 最终就会出现像size=-1这种诡异的状态,甚至抛出
ConcurrentModificationException或者NoSuchElementException。
重启服务器后,集合被重新初始化,状态恢复正常,但只要并发场景存在,问题肯定会再次出现。
解决方案:修复线程安全问题
方案1:替换为线程安全的集合
把ArrayList换成CopyOnWriteArrayList,它是ArrayList的线程安全变体,适合读多写少的在线用户追踪场景:
public class OnlineUserTracker { private final List<User> users = new CopyOnWriteArrayList<>(); public OnlineUserTracker() {} // 不要直接暴露集合引用,提供封装的添加方法 public void addUser(User user) { // 先检查是否已存在,避免重复添加 boolean exists = users.stream().anyMatch(u -> u.getUsername().equals(user.getUsername())); if (!exists) { users.add(user); } } // 返回不可修改的视图,防止外部直接修改集合 public List<User> getUsers() { return Collections.unmodifiableList(users); } public User getUserFromUsername(String username) { for (User user : users) { if(user.getUsername().equals(username)) { return user; } } return null; } public void removeUser(String username) { User user = getUserFromUsername(username); if (user != null) { users.remove(user); } } }
方案2:用Map优化查找效率+线程安全
如果在线用户数量较多,遍历ArrayList查找用户效率很低,建议改用ConcurrentHashMap存储(key为username,value为User),既保证线程安全,又把查找时间复杂度降到O(1):
public class OnlineUserTracker { private final ConcurrentHashMap<String, User> users = new ConcurrentHashMap<>(); public OnlineUserTracker() {} public void addUser(User user) { // 原子操作,避免重复添加 users.putIfAbsent(user.getUsername(), user); } public Collection<User> getUsers() { return Collections.unmodifiableCollection(users.values()); } public User getUserFromUsername(String username) { return users.get(username); } public void removeUser(String username) { users.remove(username); } }
额外修复:调整AccessControlServiceImpl的调用
不要直接获取集合并修改,而是调用OnlineUserTracker封装的方法:
public boolean checkAuthorized(String username) { User user = userDao.findById(username); // Other logic userTracker.addUser(user); // 改用封装的addUser方法 // If user is authorized }
调试异常的修复
检查User类的toString()方法:
- 确保所有引用的字段都做了null判断,比如:
@Override public String toString() { return "User{" + "username='" + username + '\'' + ", role=" + (role != null ? role.getName() : "null") + '}'; } - 避免循环引用,如果存在双向引用,在
toString()里只打印id或者名称即可。
总结
你遇到的所有异常都是多线程并发修改非线程安全集合导致的内部状态损坏。只要解决了线程安全问题,这些诡异的现象就不会再出现了。
内容的提问来源于stack exchange,提问作者Jai

