Java使用并发List仍保留请求数据的原因及递归场景下的解决方法
问题解决思路
问题根源
- 你当前定义的
menuDTOList是Demo类的实例成员变量,只要Demo类实例没有被销毁,列表中存储的数据就会一直留存,多次调用getData方法都会往同一个列表中追加数据,自然会保留上一次请求的残留内容。 Collections.synchronizedList仅用来解决多线程并发修改列表的线程安全问题,完全不解决多次请求复用同一个列表实例的问题,所以对你的场景无效。- 如果你在递归调用过程中清空列表,会把当前请求已经加入列表的有效数据也一并清除,自然无法正常生效。
最优解决方案(推荐)
将列表改为递归方法的入参传递,每次请求调用getData时新建空列表,再传递给递归方法,完全不依赖类成员变量,既可以保留原有递归逻辑,还能天然隔离不同请求的数据,没有历史数据残留问题。
修改后的代码如下:
public class Demo { // 移除原有的成员变量menuDTOList protected Iterable<MenuDTO> getData() { // 每次请求新建独立的空列表,不同请求完全隔离 List<MenuDTO> menuDTOList = Collections.synchronizedList(new ArrayList<MenuDTO>()); for (...) { // 调用methodA时传入当前请求的列表 methodA(你的menuItemExpandedDTO实例, menuDTOList); } return menuDTOList; } // 新增List<MenuDTO>类型的入参 private void methodA(final MenuItemExpandedDTO menuItemExpandedDTO, List<MenuDTO> menuDTOList) { menuDTOList.add(dto); for (...) { // 调用methodB时传入当前请求的列表 methodB(menuDTOList); } } // 新增List<MenuDTO>类型的入参 private void methodB(List<MenuDTO> menuDTOList) { for (...) { // 调用methodA时传入当前请求的列表 methodA(dto, menuDTOList); } } }
该方案优势:
- 不存在历史数据残留问题,每次请求都是独立的新列表
- 天然线程安全,多个请求同时调用也不会互相干扰
- 不需要额外执行清空操作,不会影响原有递归逻辑
备选方案(仅适用于无法修改递归方法入参的场景)
如果受限于现有代码规范无法修改methodA和methodB的入参,可以用ThreadLocal存储每个线程独立的列表,每次请求初始化新列表,用完手动清除避免内存泄漏,代码如下:
public class Demo { // 用ThreadLocal存储每个线程独立的列表实例 private ThreadLocal<List<MenuDTO>> menuDTOListHolder = ThreadLocal.withInitial(ArrayList::new); protected Iterable<MenuDTO> getData() { try { // 每次请求先清空当前线程的列表 menuDTOListHolder.get().clear(); for (...) { methodA(你的menuItemExpandedDTO实例); } // 返回列表拷贝,避免后续修改影响返回结果 return new ArrayList<>(menuDTOListHolder.get()); } finally { // 用完手动移除,避免内存泄漏 menuDTOListHolder.remove(); } } private void methodA(final MenuItemExpandedDTO menuItemExpandedDTO) { menuDTOListHolder.get().add(dto); for (...) { methodB(); } } private void methodB() { for (...) { methodA(dto); } } }
内容的提问来源于stack exchange,提问作者user17188729
相关产品推荐
相关产品推荐

