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() 之后的线程会立即回到就绪状态,而不是等待状态。

相关推荐
诺诺Okami6 小时前
Android Framework-Launcher-UI和组件
android
架构师沉默6 小时前
设计多租户 SaaS 系统,如何做到数据隔离 & 资源配额?
java·后端·架构
潘潘潘7 小时前
Android线程间通信机制Handler介绍
android
潘潘潘7 小时前
Android动态链接库So的加载
android
Java中文社群7 小时前
重要:Java25正式发布(长期支持版)!
java·后端·面试
潘潘潘7 小时前
Android多线程机制简介
android
每天进步一点_JL8 小时前
JVM 类加载:双亲委派机制
java·后端
用户298698530149 小时前
Java HTML 转 Word 完整指南
java·后端
渣哥9 小时前
原来公平锁和非公平锁差别这么大
java