学 Java 多线程第一周,遇到一个很尴尬的问题——线程开了,代码在里面跑着,然后我想让它停下来。翻书,书上告诉我:不要用 Thread.stop(),这个方法已经被标记为废弃了。
但是为什么呢?为什么一个叫"停止"的方法不能用?那用什么?
带着这个问题我搜了好几天。现在把学到的东西写下来。如果你也在学多线程,可能用得上。
第一个方法:搞一个开关变量,自己控制
最直观的想法是这样的:开一个循环,在循环里检查一个变量,变量变了就退出。
publicclassSafeStopWithFlagimplementsRunnable{privatevolatilebooleanrunning=true;@Overridepublicvoidrun(){while(running){try{System.out.println("线程正在跑...");Thread.sleep(1000);}catch(InterruptedExceptione){running=false;Thread.currentThread().interrupt();}}System.out.println("线程安全停下来了。");}publicvoidstopThread(){running=false;}}这里有个坑我一开始没注意到:running变量必须要加volatile。原因是两个线程可能在不同的 CPU 核心上跑,不加 volatile 的话,一个线程改了 running,另一个线程可能"看不见"。
举个例子:你在主线程把 running 改成 false 了,工作线程还在读自己缓存里的 running,还是 true,停不下来。
volatile 解决的就是这个问题——保证一个线程改了,另一个线程立刻能看到。
用的时候也很简单:
SafeStopWithFlagtask=newSafeStopWithFlag();Threadthread=newThread(task);thread.start();// 过一会,让它停Thread.sleep(3000);task.stopThread();这个办法的好处是简单,坏处是如果线程正在做 sleep 或者 wait 这种阻塞操作,你改了 running 它也不会立刻知道——它要等阻塞结束、下一次循环检查的时候才退出。
第二个方法:用 Java 自带的中断机制
学到这里我遇到了第二个办法,也是 Java 官方推荐的做法。说实话我一开始没太理解,感觉和第一个方法差不多。
Java 的每个线程都有一个"中断标志位",就是一个布尔值。中断机制说白了就是:
- 调用
thread.interrupt()— 把标志位设成 true - 线程自己去检查
Thread.currentThread().isInterrupted()— 看看标志位是不是 true - 如果是,就主动退出
说起来很简单。但有个地方特别绕:
有一些方法(比如 sleep()、wait()、join())在等待的时候会去检查这个中断标志。如果它们发现中断了,会做两件事:抛出InterruptedException,并且把中断标志清掉(重新设成 false)。
这就导致了一个很反直觉的情况:
// 想象一下这个场景thread.interrupt();// 我设置了中断// 此时 interrupt 标志是 trueThread.sleep(1000);// sleep 发现标志是 true// 抛出 InterruptedException// 把标志清回 false所以当你捕获到InterruptedException之后,如果还想让上层的代码知道"我被中断了",就得在 catch 里再调用一次 interrupt():
publicclassInterruptExampleimplementsRunnable{@Overridepublicvoidrun(){while(!Thread.currentThread().isInterrupted()){try{System.out.println("工作中...");Thread.sleep(1000);}catch(InterruptedExceptione){// sleep 被中断的时候,中断标志已经被清掉了// 所以这里要重新设回来System.out.println("睡觉的时候被叫醒了!");Thread.currentThread().interrupt();break;}}System.out.println("线程被中断信号停掉了。");}}调用的时候就这样:
Threadthread=newThread(newInterruptExample());thread.start();Thread.sleep(3000);thread.interrupt();// 就这一句这个办法一开始让我很困惑——为什么不直接用第一个方法的开关变量?后来我理解了两者的区别:
如果线程正在 sleep 里面睡觉,你用running = false去停它,它要等这一轮 sleep 结束、下一次循环才开始检查。而interrupt()可以直接把 sleep “叫醒”,然后立刻响应。对于有阻塞操作的代码,中断机制更及时。
第三个方法:用线程池的时候怎么办
前面两个方法都是自己 new Thread()。但现在写项目大多用线程池(ExecutorService)。线程池的场景下,你提交一个任务,得到一个 Future 对象,可以用它来取消任务。
publicclassFutureCancelDemo{publicstaticvoidmain(String[]args){ExecutorServiceexecutor=Executors.newSingleThreadExecutor();Future<?>future=executor.submit(()->{while(!Thread.currentThread().isInterrupted()){System.out.println("任务运行中...");try{Thread.sleep(1000);}catch(InterruptedExceptione){System.out.println("任务被中断了。");Thread.currentThread().interrupt();}}});try{Thread.sleep(3000);future.cancel(true);// true 表示发送中断信号}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{executor.shutdown();}}}其实关键就是future.cancel(true)这一句。传 true 会在底层调用线程的 interrupt() 方法。所以任务代码里还是需要正确处理中断才能配合上。
第四个方法:碰到不接受中断的操作怎么办
学到前面三个方法的时候,我以为我已经全懂了。直到我遇到了ServerSocket.accept()——这个方法会一直阻塞着等客户端连接,但它不听 interrupt() 的信号。
我试过:
// 一个线程在 accept 那里等着// 另一个线程去 interrupt() 它// 结果:它不动。interrupt 电不到它。这就很让人头疼。网上查了一下,很多人说这是"不可中断的阻塞操作"。传统的 I/O 操作和 synchronized 锁都属于这一类。
那怎么办?
思路是这样的:既然你电不动它,我就把它的资源直接关了。资源一关,阻塞操作会抛出 IOException,线程就能响应了。
publicclassSocketHandlerimplementsRunnable{privatefinalServerSocketserverSocket;publicSocketHandler(ServerSocketserverSocket){this.serverSocket=serverSocket;}@Overridepublicvoidrun(){try{while(!Thread.currentThread().isInterrupted()){// 这行会一直等,不接受 interruptSocketsocket=serverSocket.accept();// 处理连接...}}catch(IOExceptione){if(Thread.currentThread().isInterrupted()){System.out.println("资源被关了,线程停下来了。");}}}publicvoidstopThread(){Thread.currentThread().interrupt();try{serverSocket.close();// 关键:关掉资源}catch(IOExceptione){System.err.println("关闭 socket 出错: "+e);}}}这个办法让我觉得,多线程编程有时候不是"写好代码就行",还得理解底层阻塞的原理,知道哪些操作响应中断、哪些不响应。
最后说一下:那些别用的方法
说实话,一开始我在网上搜的时候,确实看到过Thread.stop()。用起来超级简单,一行代码线程就死了。但我为什么不能用它呢?
我看了很多文章才理解里面有什么坑:
Thread.stop()会在线程执行的任意位置强行终止它,然后立即释放它持有的所有锁。释放锁听起来是好事,但问题是,stop 不会管这个对象在释放锁的时候处于什么状态。举个例子:你正在往一个 List 里加数据,加到一半,stop 来了,数据还没加完,锁被释放了。这时候别的线程读到这个 List,看到的是半个数据——不完整、不一致。这种问题很难排查。
Thread.suspend()和Thread.resume()是相反的问题——挂起的时候不释放锁,如果拿到锁的线程被挂起了,其他线程等着这把锁,整个程序就死锁了。
所以它们被废弃是有理由的。虽然用起来方便,但不能碰。
简单总结
| 场景 | 怎么做 |
|---|---|
| 线程里就是纯计算,没有阻塞操作 | 加个 volatile 变量当开关 |
| 有 sleep、wait 这类操作 | 用 interrupt() 来停 |
| 用线程池管理任务 | 用 Future.cancel(true) |
| 遇到传统 I/O 等阻塞 | 关掉资源让它抛异常 |
学完这些我最大的感受是:线程的停止不是"杀死",而是"协商"。你告诉它该停了,它自己决定什么时候整理好东西停下来。这不是 Java 的设计缺陷,恰恰是它安全的原因。
如果有人问我 Java 里怎么停掉一个线程,我现在会说:Java 不支持强行停线程。它只支持你告诉它"我想让你停",然后它自己安全地停。