You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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解决了什么,还缺什么?

你修改后的版本做了两个关键修复:

  1. 给NotifyingThread添加了带索引的构造方法,把循环的idx传递给线程内部,彻底避免了闭包陷阱,每个线程都能正确获取对应的args[idx]。
  2. 修正了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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 14:02:35