Java多线程NotifyListener行为探究:InvokeThread版本输出差异及同步机制疑问
问题分析:版本2偶尔无输出的根源及后续同步问题澄清
先从原始版本2的问题说起,再聊修改后的版本还存在的隐患:
一、原始版本2偶尔无输出的核心原因
1. 硬编码索引+闭包陷阱的双重错误
看你原始版本2的循环代码:
for(int idx=0;idx<argsCount;idx++) { NotifyingThread thread = new NotifyingThread() { @Override public void doRun() { synchronized (this) { threadList = this.getName() + "," + threadList; synList.addAll(Arrays.asList(args[1].split(","))); // 这里直接写死了args[1]! } } }; // ... }
首先,不管循环的idx是0还是1,所有线程都硬编码读取args[1],这本身就不符合逻辑。就算你改成args[idx],在Java 8之前,循环变量idx不是final/effectively final,闭包会导致所有线程共享同一个idx的引用,最终所有线程都会用循环结束后的idx值(也就是argsCount),直接抛出数组越界异常——不过你说的是偶尔无输出,这说明还有更隐蔽的线程安全问题。
2. 共享变量的竞态条件与内存可见性问题
你的threadList和threadNotifyCount是InvokeThread的成员变量,被多个线程同时读写,但完全没有同步保护:
threadList的非原子修改:threadList = this.getName() + "," + threadList;是读-改-写的非原子操作,多个线程同时执行时,会互相覆盖对方的修改。比如两个线程同时读取到空的threadList,各自拼接后写回,最终threadList只会保留其中一个线程的名字,导致_temp.split(",").length是1,而threadNotifyCount最终会变成2,这时候threadNotifyCount == 长度的条件永远不成立,自然不会打印输出。threadNotifyCount的计数丢失:threadNotifyCount++也是非原子操作,多个线程同时读取同一个值、加1、写回,会导致计数丢失。比如两个线程同时读取到0,都加1后写回,结果还是1,永远到不了2,同样触发不了打印逻辑。- 内存可见性问题:没有
volatile修饰或同步块,线程对threadList和threadNotifyCount的修改,其他线程可能看不到最新值,比如notifyOfThreadComplete里读取的threadList还是旧值,导致判断条件错误。
二、修改后的版本2.1解决了什么,还缺什么?
你修改后的版本做了两个关键修复:
- 给
NotifyingThread添加了带索引的构造方法,把循环的idx传递给线程内部,彻底避免了闭包陷阱,每个线程都能正确获取对应的args[idx]。 - 修正了
args索引的硬编码问题,现在线程能处理各自的参数了。
但同步问题依然没有彻底解决:
- 你在
doRun里用的synchronized (this),这里的this是每个NotifyingThread的实例,不是InvokeThread的实例,所以多个线程的同步块锁的是不同对象,完全无法保护threadList和threadNotifyCount的共享读写。 notifyOfThreadComplete里的threadNotifyCount++依然是无同步的非原子操作,还是会有计数丢失的风险。threadList的读写依然没有同步,还是会出现覆盖和内存可见性问题。
三、彻底解决同步问题的建议
要根治这些隐患,可以做以下调整:
1. 用原子类解决计数问题
把int threadNotifyCount改成AtomicInteger threadNotifyCount = new AtomicInteger(0);,然后在notifyOfThreadComplete里用原子方法递增:
@Override public void notifyOfThreadComplete(NotifyingThread thread) { int currentCount = threadNotifyCount.incrementAndGet(); synchronized (lock) { String _temp = threadList.substring(0, threadList.length()-1); if(currentCount == _temp.split(",").length) { Collections.sort(synList); System.out.println(thread.getName() + ": " + synList); } } }
2. 统一锁保护共享变量的读写
在InvokeThread里定义一个全局锁对象,所有对threadList的读写都在这个锁的同步块里:
public class InvokeThread implements ThreadCompleteListener{ private final Object lock = new Object(); // 全局锁 List<String> synList = Collections.synchronizedList(new ArrayList<String>()); AtomicInteger threadNotifyCount = new AtomicInteger(0); String threadList = ""; // ... @Override public void doRun(int idx) { synchronized (lock) { // 用全局锁保护threadList的修改 threadList = this.getName() + "," + threadList; } synList.addAll(Arrays.asList(args[idx].split(","))); } @Override public void notifyOfThreadComplete(NotifyingThread thread) { int currentCount = threadNotifyCount.incrementAndGet(); synchronized (lock) { // 用全局锁保护threadList的读取 String _temp = threadList.substring(0, threadList.length()-1); if(currentCount == _temp.split(",").length) { Collections.sort(synList); System.out.println(thread.getName() + ": " + synList); } } } }
这样就能保证所有共享变量的读写都是线程安全的,彻底避免竞态条件和内存可见性问题。
内容的提问来源于stack exchange,提问作者Srii
相关产品推荐
相关产品推荐

