[多线程] - 如何优雅的停止一个线程

一、什么时候我们需要中断一个线程

在实际的开发中,有很多场景需要我们中断一个正在运行的线程,就比如:

  1. 当我们使用抢票软件时,其中某一个通道已经抢到了火车票,这个时候我们就需要通知其他线程停止工作。
  2. 当我们希望在一个限定的时间里获得任务结果的时候,也需要在任务超时的时候关闭它

但是Java并不能像代码块中的break一样干脆的退出线程运行,Java本身提供的API方法总是让人觉得差强人意,没有办法完美的解决我们的需求,那么我们该如何优雅的停止一个线程呢?

二、如何终止一个线程

1) 暴力停止

1. 使用stop方法停止线程

Java在停止线程的运行方面,提供了一个stop的方法,首先我们写段代码,演示stop方法是如何终止线程的:

package xiao.thread.stop;

public class StopThread {
    
    
	private static int count;

	public static void main(String[] args) throws InterruptedException {
    
    
		// first :create a thread task
		Thread thread = new Thread(() -> {
    
    
			while (true) {
    
    
				++count;
				// 为了减少打印数 增加个计数判断 
				//也可调用sleep方法进行休眠
				if (count % 100 == 0) {
    
    
					System.out.println("此时线程依旧存活" + count);
				}
			}
		}, "thread_task for stopThread");
		thread.start();
		// 如果直接终止 很有可能thread的run方法还没有开始执行 所以建议让main线程休眠一段时间来观察效果
		Thread.currentThread().sleep(1_000);
		thread.stop();
	}
}

首先我们可以明显的观察到,当我们调用stop的时候,线程被暴力停止了。这种方式虽然简单,但是java语言本身并不提倡我们这么做。stop()方法呈删除线状态,已经表明了这个方法已经被弃用。

2. 为什么不提倡使用stop方法

其实不提倡使用stop方法的原因很简单,因为线程最优状态的终止是只能自杀而不能被杀。具体的来说就是:当我们通过其他线程调用stop方法时,此刻我们并不知道被杀死的线程执行到了哪里。就比如我们在做一个为集合添加数据的操作,我们此时无法知道数据的添加进行到了哪一步。而当我们调用stop方法,此时被杀死的线程会立即释放自身持有的锁,其他线程此时就可以看到未被处理完的数据,造成线程安全的问题,破坏了对象的一致性。

2) 捕获异常法

1. 中断机制

中断很好理解,它本身其实并不具备中断的能力,只是一种协作机制。在java中并没有对中断进行任何语法层面的实现,只能够靠我们利用已有的中断标识来记性业务处理达到中断的目的。

2. 相关API

在这里插入图片描述
在实现中断功能时,我们常用的API主要是三个方法:

1. public void interrupt
调用此方法会将线程的中断标识设为true
2. public boolean isInterrupt
调用此方法将会返回此刻现成的中断标识
3.public static boolean interruped
该方法只能通过Thread.interrupted()调用,他会做两个操作
第一步返回当前线程的中断状态
第二部将当前现成的中断标识设为false

3. 证实interrupt没有停止线程的能力

首先我们举个小例子来证明当我们调用interrupt方法时,jvm无法为我们中断目标线程:

扫描二维码关注公众号,回复: 12338629 查看本文章
public class InterruptThread {
    
    

	private static int count;

	public static void main(String[] args) {
    
    
		Thread thread = new Thread(() -> {
    
    
			while (true) {
    
    
				System.out.println("线程依旧存活:_此时计数器状态为_" + ++count);
				try {
    
    
					Thread.currentThread().sleep(1_000);
				} catch (InterruptedException e) {
    
    
				}

			}
		},"测试线程");
		thread.start();
		thread.interrupt();
	}

}

我们看下输出结果:
在这里插入图片描述
虽然我们在调用start方法后立即调用了interrupt,但是目标的测试线程依旧每隔一秒在控制台打印了计数器状态,并没有实际的中断线程,通过这个例子我们证明了在我们调用interrupt的时候,jvm并没有立即为我们停止目标线程的运行,如果我们想要在调用interrupt之后停止线程该如何做呢?

4. 异常处理法停止线程

既然我们证实了jvm确实不会主动地替我们中断线程,那么我们就需要利用interrupt方法中提到的中断标识来做一些事情。首先我们需要证明一个新的论点:

当阻塞方法接收到中断信号的时候,会抛出一个InterruptedException 异常

那么我们改造下上面的代码:

public class InterruptThread {
    
    
	private static int count;
	public static void main(String[] args) throws InterruptedException {
    
    
		Thread thread = new Thread(() -> {
    
    
			while (true) {
    
    
				System.out.println(Thread.currentThread().getName()+"正在运行~");
				try {
    
    
					Thread.currentThread().sleep(2_500);
				} catch (InterruptedException e) {
    
    
					// TODO Auto-generated catch block
					e.printStackTrace();
					System.out.println(Thread.currentThread().getName()+"接收到中断异常");
				}
			}

		}, "测试线程");
		thread.start();
		Thread.currentThread().sleep(1_000);
		thread.interrupt();
	}
}

运行结果:
在这里插入图片描述
在这里我们可以看出,当我们调用interrupt方法时,由于线程此时也调用了可能将线程阻塞的方法sleep,因此此时目标线程在收到我们的中断信号的时候抛出了interruptException,但是由于while方法体里面还有代码需要执行,线程此时并没有结束,这个时候我们就需要利用我们捕获到的异常改造下代码:

public class InterruptThread {
    
    
	private static int count;
	private static boolean mark = true ;
	public static void main(String[] args) throws InterruptedException {
    
    
		Thread thread = new Thread(() -> {
    
    
			while (mark) {
    
    
				System.out.println(Thread.currentThread().getName()+"正在运行~");
				try {
    
    
					Thread.currentThread().sleep(2_500);
				} catch (InterruptedException e) {
    
    
					// TODO Auto-generated catch block
					e.printStackTrace();
					mark = false ;
					System.err.println(Thread.currentThread().getName()+"接收到中断信号");
				}
			}
		}, "测试线程");
		thread.start();
		Thread.currentThread().sleep(1_000);
		thread.interrupt();
	}
}

运行结果:
在这里插入图片描述
可以看出程序在接收到我们的中断信号后就没有继续运行,线程停止。而上面代码的变化其实就发生在while的判断条件上,我们加入了一个布尔变量mark,通过捕获异常停止线程的原理其实也很简单:我们认为的设置一个中断变量,将该值设置为代码运行的入口条件,当我们捕获到异常的时候,改变中断变量的值以达到跳出循环的目的从而实现停止线程。
当然,实现跳出循环的方式有很多,我们也可以通过return关键字实现跳出循环的效果:

public class InterruptThread {
    
    
	public static void main(String[] args) throws InterruptedException {
    
    
		Thread thread = new Thread(() -> {
    
    
			while (true) {
    
    
				System.out.println(Thread.currentThread().getName()+"正在运行~");
				try {
    
    
					Thread.currentThread().sleep(2_500);
				} catch (InterruptedException e) {
    
    
					e.printStackTrace();
					System.err.println(Thread.currentThread().getName()+"接收到中断信号");
					return ;
				}
			}
		}, "测试线程");
		thread.start();
		Thread.currentThread().sleep(1_000);
		thread.interrupt();
	}
}

运行结果:
在这里插入图片描述
通过return也可以很好地利用异常来结束线程的运行。

当然,我们还有个关于interruptException异常的问题要说下

在实际的开发中,我们不能不管不顾的通过抛出异常的方式来结束线程,如果你只是想记录日志,那么此时应该将中断标记重新置为false,以避免程序意外中断

3) 通过守护线程实现(了解即可)

我们也可以利用守护线程的特性来结束一个线程的运行

如果不了解守护线程的特性,可以看我之前的文章
线程的简介

下面我们先看代码实现:

public class DaemonThreadInterrupt {
    
    
	public static Thread thread;
	public boolean flag = true;

	public void execut(Runnable task) {
    
    
		thread = new Thread(()->{
    
    
			Thread thread2 = new Thread(task,"任务线程");
			thread2.setDaemon(true);
			thread2.start();
			try {
    
    
				thread2.join();
			} catch (InterruptedException e) {
    
    
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		},"执行线程");
		thread.start();

	}

	public void stop() {
    
    
		thread.interrupt();
	}
	
	public static void main(String[] args) {
    
    
		DaemonThreadInterrupt daemonThreadInterrupt = new DaemonThreadInterrupt();
		daemonThreadInterrupt.execut(()->{
    
    
			while(true) {
    
    
				System.out.println("程序运行");
				try {
    
    
					thread.sleep(1_000);
				} catch (InterruptedException e) {
    
    
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		});
		daemonThreadInterrupt.stop();
	}
}

实现原理:
在上述代码中,我将需要实现的任务线程放到了一个执行线程中,执行线程不负责任何业务逻辑的处理,只负责启动任务线程。而任务线程在启动后,将他设为了执行线程的守护线程。然后让他join到执行线程中。
然后接下来我们就利用了join方法的特性以及守护线程的特点设计了stop方法:
执行join方法后,执行线程将等待任务线程执行完毕后才会执行,但是当我们调用interrupt方法时,join方法接收到中断信号就会抛出异常,此时执行线程不在等待任务线程的运行。重点来了:此时执行线程执行完毕,任务线程作为执行线程的守护线程也结束了他的生命周期。
这种方式充分利用了线程的多种特性停止了一个线程,而且结束的过程可控,也可设置延时结束,适合用作定时任务线程的实现。

猜你喜欢

转载自blog.csdn.net/xiaoai1994/article/details/110041270