Java 线程通信基础:interrupt、wait 和 notifyAll 详解

前言

线程间的交互方式一般有:

  1. 一个线程启动另一个线程。
  2. 一个线程终结另一个线程。
  3. 多个线程相互配合完成任务。

第一种我们已经比较熟悉了,来看第二种和第三种。

stop() 和 interrupt()

java 复制代码
Thread thread = new Thread() {
    @Override
    public void run() {
        for (int i = 0; i < 1_000_000; i++) {
            System.out.println("i = " + i);
        }
    }
};
thread.start();

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}
thread.stop();

之前,我们可以使用 Thread.stop() 来停止线程,但它已经被弃用了。甚至方法实现都被注释掉了,始终会抛出 UnsupportedOperationException 异常。

java 复制代码
@Deprecated(since="1.2")
public final void stop() {
    /*
    ... 源码实现 ...
    */
    throw new UnsupportedOperationException();
}

为什么呢?

并不是因为它无效而被弃用,而是因为它突然终止线程,不管线程当前在做什么,都会被强制停止,这会导致数据不一致问题。

比如一个任务只执行到了一半时就被终止,会让持有的锁无法被释放,非常危险。

解决办法是使用中断(Interrupt)机制,即使用 Thread.interrupt(),像这样:

java 复制代码
Thread thread = new Thread() {
    @Override
    public void run() {
        for (int i = 0; i < 1_000_000; i++) {
            System.out.println("i = " + i);
        }
    }
};
thread.start();

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}
thread.interrupt();

运行会发现,子线程还是从 0 打印到了 999999,难道这个方法没起作用吗?

其实 interrupt() 只会将线程标记为中断状态,它是一个温和的通知,告诉线程:"我希望你结束"。但是否结束,是由线程内部决定的。

我们就来支持一下:

java 复制代码
Thread thread = new Thread() {
    @Override
    public void run() {
        for (int i = 0; i < 1_000_000; i++) {
            // 检查中断状态
            if (isInterrupted()) {
                // 线程被中断,执行收尾工作
                // ...
                System.out.println("thread is interrupted");
                return; // 响应中断,直接退出
            }
            System.out.println("i = " + i);
        }
    }
};

再次运行就能看到:

less 复制代码
...
i = 829359
i = 829360
thread is interrupted

其中 isInterrupted() 会返回是否被打断的标记,如果被其他线程中断了,就会返回 true。我们通常会在各种耗时操作的前面进行判断,以免无意义的资源浪费,因为都要被终止了。

Thread.interrupted() 也能够进行判断,与 isInterrupted() 不同的是,它内部会将标记置为 false

补充:为什么 Thread.sleep() 需要捕获 InterruptedException 异常?

因为处于等待(Waiting)状态(如 sleep, wait, join)的线程也是可以被终止的。

子线程在睡眠时被打断,并不会导致状态不一致。因此,为了让线程能够立刻中断,JVM 会通过抛出 InterruptedException 异常的方式来立即唤醒子线程,我们可以在 catch 分支中进行收尾工作。

另外,当抛出 InterruptedException 时,会清除线程中断的标志位,即将标记置为 false。如果我们想要在后续代码中,依然能够检测到中断,需要在 catch 分支中将中断状态设为 trueThread.currentThread().interrupt())。

java 复制代码
Thread thread = new Thread() {
    @Override
    public void run() {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            // ... 执行收尾工作 ...
            System.out.println("interrupted");
            // 此时中断标志位已被清除为 false
            // 如果希望后续代码继续响应中断,需要再次设置
            Thread.currentThread().interrupt(); 
        }
    }
};
thread.start();

try {
    Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
thread.interrupt();

wait() 和 notifyAll()

我们再来看看第三种:两个线程的配合操作。来看一个例子:

java 复制代码
public class WaitDemo {
    private String data;

    public synchronized void initData() {
        this.data = "data";
    }

    public synchronized void printData() {
        System.out.println("data is " + data);
    }

    public void run() {
        Thread threadA = new Thread() { // 打印线程
            @Override
            public void run() {
                try {
                    Thread.sleep(1000); // 1秒后打印
                } catch (InterruptedException ignored) {
                }
                printData();
            }
        };
        Thread threadB = new Thread() { // 初始化线程
            @Override
            public void run() {
                try {
                    Thread.sleep(2000); // 2秒后初始化
                } catch (InterruptedException ignored) {
                }
                initData();
            }
        };
        threadB.start();
        threadA.start();
    }
}

现在运行,结果会是 data is null,因为 threadA 打印时,threadB 还未初始化 data

往往一个线程的工作,需要另一个线程的执行结果,这时我们可以在 printData 中不断检查 data 的值,直至不为空。

java 复制代码
public synchronized void printData() {
    while (data == null){
        // 空转,等待 data 被赋值
    }
    System.out.println("data is " + data);
}

这样有个问题:printData() 方法取得了锁,它会导致其他线程无法进入 initData() 来修改 data 的值,造成死锁。

这个时候需要使用 wait() 方法,它会让当前线程释放持有的锁,并让当前线程进入等待池中(不是锁池),使自己处于等待状态,不占用 CPU。

java 复制代码
public synchronized void printData() {
    // 这里必须使用 while 循环
    while (data == null) {
        try {
            wait(); // 释放锁,进入等待池
        } catch (InterruptedException e) {
            // 执行收尾工作
            System.out.println("thread is interrupted");
            // 中断标志位同样会被清除
            Thread.currentThread().interrupt();
            return;
        }
    }
    System.out.println("data is " + data);
}

线程可能会在未被 notify 的情况下醒来,此时 data 仍为 null,使用 while 能确保只有在满足条件的情况下,才能执行后续代码。

怎么通知等待线程呢?只需调用 notifynotifyAll 方法即可。它会让处于等待池中的线程被唤醒,转变为可运行状态,再重新进入锁池去竞争取得锁,竞争到锁的线程会从 wait() 方法处返回,继续执行。

java 复制代码
public synchronized void initData() {
    this.data = "data";
    notifyAll(); // 唤醒所有在等待池中的线程
}

不过,notify 只会唤醒一个等待的线程,我们更多会调用 notifyAll(),来唤醒所有的等待线程。

现在运行的结果将会是 data is data

注意:waitnotifyAll 必须配合使用,否则会导致线程进入无休止的等待。

另外,waitnotifyAll 都是 Object 类中的方法,并不是 Thread 类中的方法。因为唤醒线程和让线程等待的主体并不是 Thread,而是当前同步块中的监视器(Monitor),也就是锁对象。

比如我们可以这样写,使用一个 Object 对象作为锁:

java 复制代码
private final Object monitor = new Object();

public void initData() {
    synchronized (monitor) {
        this.data = "data";
        monitor.notifyAll(); // 唤醒所有等待 monitor 锁的线程
    }
}

public void printData() {
    synchronized (monitor) {
        while (data == null) {
            try {
                monitor.wait(); // 在 monitor 对象上等待
            } catch (InterruptedException e) {
                // 执行收尾工作
                System.out.println("thread is interrupted");
                Thread.currentThread().interrupt();
                return;
            }
        }
        System.out.println("data is " + data);
    }
}

join()

Thread.join() 方法用于将指定线程放在当前线程之前执行,当前线程将被挂起(进入等待状态),直至指定线程执行完毕。

它本质上也是一种等待,所以调用 join() 的线程如果在等待期间被中断,也会抛出 InterruptedException 异常。

例如,这样也能正常工作:

java 复制代码
private String data;

public synchronized void initData() {
    this.data = "data";
}

public synchronized void printData() {
    System.out.println("data is " + data);
}


public void run() {
    Thread threadB = new Thread() {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException ignored) {
            }
            initData();
        }
    };
    Thread threadA = new Thread() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ignored) {
            }
            try {
                // 等待 threadB 执行完毕
                threadB.join();
            } catch (InterruptedException e) {
                // 如果 threadA 在等待时被中断
                System.out.println("threadA waiting for B was interrupted");
                Thread.currentThread().interrupt(); // 重新设置中断状态
                return;
            }
            printData();
        }
    };

    threadB.start();
    threadA.start();
}

不过 waitnotify 的方式更灵活,还是更加常用的。

yield()

Thread.yield() 在线程内部调用,它用于提示线程调度器将 CPU 时间片让给同优先级的其他线程。

yield() 之后的线程会立即回到就绪状态,而不是等待状态。

相关推荐
侠客行03175 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪5 小时前
深入浅出LangChain4J
java·langchain·llm
老毛肚6 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎7 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
Doro再努力7 小时前
【Linux操作系统10】Makefile深度解析:从依赖推导到有效编译
android·linux·运维·服务器·编辑器·vim
Yvonne爱编码7 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚7 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
Daniel李华7 小时前
echarts使用案例
android·javascript·echarts
你这个代码我看不懂7 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
fuquxiaoguang7 小时前
深入浅出:使用MDC构建SpringBoot全链路请求追踪系统
java·spring boot·后端·调用链分析