循环创建线程导致变量j连续重复打印问题排查
首先,咱们来拆解你遇到的j变量打印重复的问题,以及当前线程使用方式的潜在隐患:
核心原因:线程执行时机与变量捕获问题
你在for循环里直接创建Thread对象,而j是一个外部作用域变量(看起来是类成员或外层方法变量)。这里的关键问题是:线程启动后并不会立即执行run()方法,而for循环会快速推进,不断修改j的值。当线程最终开始执行时,它们读取的j已经是循环更新后的最新值,而非线程创建时的那个值。这就导致多个线程打印出相同的j值,看起来像是重复输出。
举个简化的例子就能明白:
double j = 0.0; for (int k = 0; k < 5; k++) { j += 0.2; new Thread(() -> System.out.println(j)).start(); }
这段代码大概率会输出多个相同的j值,因为线程启动后,循环已经把j更新到了1.0,所有线程都读取到了这个最终值。
另外,你当前的线程使用方式还有这些问题:
- 循环创建大量线程,会导致系统上下文切换频繁,消耗过多资源,甚至可能触发系统的线程数限制。
finally块里只等待最后一个copythread终止,之前创建的线程根本没被等待,会导致后续的统计(比如successRateCopy)不准确,因为可能还有线程在后台运行。j的修改没有同步机制,虽然double读写是原子操作,但多个线程并发读取时可能存在可见性问题(线程看不到其他线程修改后的j值)。
针对性解决方案
1. 捕获循环内的局部变量快照
在for循环内部,创建一个局部变量保存当前的j值,让每个线程捕获这个局部变量,这样每个线程拿到的就是创建时的j值,不会被后续循环修改:
for (String row : finallist) { a++; j = j + i; // 保存当前j的快照到局部变量 double currentJ = j; if (a % 10 == 0) { Thread.sleep(1500); } System.out.println(a); copythread = new Thread("" + a) { public void run() { try { // 打印局部变量currentJ,而非外部的j System.out.println(currentJ); javafx.application.Platform.runLater(() -> copyGauge.setValue(currentJ * 100)); // ... 其他复制逻辑 } catch (Exception ex) { // 异常处理 } } }; copythread.start(); }
2. 使用线程池管理并发,替代手动创建线程
手动创建大量线程不是最佳实践,建议用ExecutorService控制并发数,比如固定大小的线程池,既能提高效率,又能避免资源耗尽:
// 初始化线程池,根据系统资源调整并发数,这里设为5 ExecutorService executor = Executors.newFixedThreadPool(5); for (String row : finallist) { a++; j = j + i; double currentJ = j; String currentRow = row; // 同样捕获当前row的快照 if (a % 10 == 0) { Thread.sleep(1500); } System.out.println(a); // 提交任务到线程池 executor.submit(() -> { try { System.out.println(currentJ); Platform.runLater(() -> copyGauge.setValue(currentJ * 100)); // 用currentRow代替row,避免变量捕获问题 InetAddress ia = InetAddress.getByName("172.217.23.206"); boolean b = ia.isReachable(2000); if (b) { // ... 复制逻辑,使用currentRow } else { Platform.runLater(() -> copyFolderLog.appendText("Cannot reach " + currentRow + "\n")); System.out.println("Cannot reach " + currentRow + "\n"); } } catch (IOException ex) { Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex); System.out.println(ex.toString()); } catch (InterruptedException ex) { Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex); } }); } // 关闭线程池,不再接受新任务 executor.shutdown(); // 等待所有任务完成,设置合理的超时时间 try { executor.awaitTermination(1, TimeUnit.HOURS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 这里再执行后续的统计、通知逻辑,确保所有任务都已完成
3. 修正进度更新的线程安全问题
如果j是用来统计整体进度的,建议使用AtomicDouble保证原子性和可见性,避免多线程下的更新问题:
// 替换原来的double j为AtomicDouble AtomicDouble j = new AtomicDouble(0.0); // 在循环里更新j j.addAndGet(i); double currentJ = j.get();
4. 修复任务等待逻辑
原来的finally块只等待最后一个线程是错误的。使用线程池的awaitTermination可以确保所有任务完成后,再执行后续的统计和通知逻辑,这样统计的successRateCopy才是准确的。
总结
你的核心问题是线程创建时的变量捕获时机导致的重复输出,通过局部变量快照可以解决。同时,改用线程池管理线程能优化性能和资源使用,修正任务等待逻辑能保证统计结果的准确性。调整后,j的打印就会和每个线程创建时的进度对应上,不会再出现重复的情况了。
内容的提问来源于stack exchange,提问作者Jakub Zoubek

