Java TimerTask异常问题:多次调度后实例值被覆盖
问题分析与解决方案
这个问题我之前也踩过坑,核心是Java里匿名内部类(包括Lambda)对外部变量的引用逻辑导致的,咱们一步步说清楚:
为什么会出现所有任务都用最后一次的实例?
你遇到的情况,大概率是你把类A的实例赋值给了一个可变的变量(比如控制器的成员变量,或者方法内被多次重新赋值的局部变量),而匿名内部类捕获的是这个变量的引用,不是变量当时的取值。
举个错误示范(对应你实际场景的问题代码):
// 错误写法:用成员变量存储类A实例,每次调用都覆盖 private A aInstance; @RequestMapping(value = "execute", method = RequestMethod.POST, produces = {MediaType.APPLICATION_JSON_VALUE}) @ResponseBody public void executeTimerTask(@RequestParam("task") String task) { // 每次调用都给成员变量赋新值 aInstance = new A(task); new java.util.Timer().schedule(new java.util.TimerTask() { @Override public void run() { // 这里引用的是成员变量的引用,执行时会取最新值 System.out.println("task: " + aInstance.getTask()); } }, 10 * 1000); }
三次快速调用后,aInstance被连续赋值三次,三个TimerTask的run方法在执行时,都会读取这个成员变量的最新值——也就是第三次创建的类A实例。
怎么解决?核心是让每个任务持有独立的参数副本
有两种简单可靠的方式:
方式1:用effectively final的局部变量捕获当前实例
在每次调度任务时,把当前的类A实例(或task参数)赋值给一个不会被重新赋值的局部变量(Java 8+里不需要显式写final,只要不修改这个变量就是effectively final),让匿名内部类捕获这个局部变量:
@RequestMapping(value = "execute", method = RequestMethod.POST, produces = {MediaType.APPLICATION_JSON_VALUE}) @ResponseBody public void executeTimerTask(@RequestParam("task") String task) { // 创建当前任务专属的局部变量,后续不再修改它 final A currentA = new A(task); // 或者如果是字符串参数,直接用task也可以,因为String是不可变的,task本身就是effectively final new java.util.Timer().schedule(new java.util.TimerTask() { @Override public void run() { System.out.println("task: " + currentA.getTask()); } }, 10 * 1000); }
这样每个TimerTask捕获的都是自己那次调用的currentA实例,互相独立,不会被后续赋值影响。
方式2:自定义TimerTask子类,通过构造函数传入参数
如果你的任务逻辑比较复杂,推荐直接写一个自定义的TimerTask子类,把需要的参数通过构造函数传入,每个任务实例自己持有参数副本:
// 自定义任务类,把需要的参数存在成员变量里 class CustomTask extends TimerTask { private final A aInstance; private final String task; // 构造函数传入当前任务的专属参数 public CustomTask(A aInstance, String task) { this.aInstance = aInstance; this.task = task; } @Override public void run() { // 直接用自己的成员变量,完全独立 System.out.println("task: " + this.task); // 这里可以用this.aInstance处理你的业务逻辑 } } // 控制器方法里使用 @RequestMapping(value = "execute", method = RequestMethod.POST, produces = {MediaType.APPLICATION_JSON_VALUE}) @ResponseBody public void executeTimerTask(@RequestParam("task") String task) { A currentA = new A(task); // 每次创建新的CustomTask实例,传入当前的参数 new java.util.Timer().schedule(new CustomTask(currentA, task), 10 * 1000); }
这种方式更清晰,尤其是当任务需要多个参数时,代码可读性更高,也不容易出错。
总结
本质就是要避免让多个TimerTask共享同一个可变变量的引用,确保每个任务持有自己独立的参数副本。上面两种方式都能解决你的问题,根据自己的业务复杂度选就行~
内容的提问来源于stack exchange,提问作者Agustn Ernesto Cardeilhac Bans
相关产品推荐
相关产品推荐

