天天看点

[Java多线程编程之三] 线程中止的多种姿势

一、错误的姿势- Stop

  • Thread.stop()
1、存在问题

  不管程序的运行逻辑如何,stop会直接中止线程,并清除监控器锁的信息,如果有些代码块的运行具有原子性,则stop可能会破坏这种原子性导致线程安全问题,所以JDK已不建议使用。

2、代码示例

  定义一个Thread的子类

StopThread

,在重写的run()中,

++i

++j

被包裹在同一同步代码块中,目的是让i和j同时加1,加锁保证同一时刻只有一个线程可以执行同步代码块,保证原子性,在理想状况下,i应该是永远等于j。

public class StopThread extends Thread {
	private int i = 0, j = 0;
	
	@Override
	public void run() {
		synchronized (this) {
			// 增加同步锁,确保线程安全
			++i;
			try {
				// 休眠10秒,模拟耗时操作
				Thread.sleep(10000L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			++j;
		}
	}
	
	// 打印i和j
	public void print() {
		System.out.println("i=" + i + " j=" + j);
	}
}
           

  但是如果在线程休眠的时候,调用了stop方法,这时i已经执行了加1操作,j尚未执行,由于线程会直接释放锁并不再执行后面的代码,会导致i和j加1操作的次数不相等,i跟j不相等,发生线程安全问题。

public class Demo3 {
	public static void main(String[] args) throws InterruptedException {
		StopThread thread = new StopThread();
		thread.start();
		// 休眠1秒,确保i变量自增成功
		Thread.sleep(1000L);
		// 暂停线程
		thread.stop();
		while(thread.isAlive()) {
			// 确保线程已经停止
		}
		// 输出结果
		thread.print();
	}
}
           

  程序执行结果:

二、正确的姿势- interrupt

  • Thread.interrupt()

      

    interrupt()

    不会粗暴地将代码执行从中间拦腰折断,而是会让被中断的线程先执行完代码,然后再抛出一个中断异常让外部程序捕获或者改变线程的状态让外部程序判断。
1、调用interrupt()导致抛出InterruptedException的情况,同时清理目标线程的中断状态
  • 目标线程正处于调用了

    wait()、wait(long)、wait(long, int)

    后的WAITING线程状态
  • 目标线程正处于调用了

    join()、join(long, int)

    后的WAITING的线程状态
  • 目标线程正处于调用了

    sleep(long, int)

    后的BLOCKED的线程状态
2、中断或返回异常值的情况

  如果目标线程是被IO或者NIO中的Channel所阻塞,同样,IO操作会被中断或者返回特殊异常值,达到中止线程的目的

3、其他情况

  设置线程的中断状态

代码示例
public class Demo3 {
	public static void main(String[] args) throws InterruptedException {
		StopThread thread = new StopThread();
		thread.start();
		// 休眠1秒,确保i变量自增成功
		Thread.sleep(1000L);
		// 暂停线程
		// thread.stop();    // 错误的中止方式
		thread.interrupt();  // 正确的中止方式
		while(thread.isAlive()) {
			// 确保线程已经停止
		}
		// 输出结果
		thread.print();
	}
}
           

  程序执行结果:

java.lang.InterruptedException: sleep interrupted
i=1 j=1
	at java.lang.Thread.sleep(Native Method)
	at chapter1.section3.ThreadStop.StopThread.run(StopThread.java:13)
           

三、适用于简单程序的姿势 - 标志位

1、用法

  一般用volatile修饰定义标志位,使其具有线程可见性;控制线程可通过控制标志位的值来控制目标线程;目标线程在其执行程序内自旋循环判断标志位的值,当值被切换时,跳出循环执行对应分支的程序。

2、代码示例

  如下面的代码所示,主线程修改flag的值,让匿名线程中止,通过控制标志位flag的值,达到控制匿名线程程序运行的效果。

public class Demo4 {
	public volatile static boolean flag = true;
	
	public static void main(String[] args) throws InterruptedException {
		new Thread(() -> {
			try {
				while (flag) {    // 判断是否运行
					System.out.println("运行中");
					Thread.sleep(1000L);				
				}
				System.out.println("线程中止");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}).start();
		// 3秒之后,将状态标志改为False,代表不继续裕兴
		Thread.sleep(3000L);
		flag = false;
		System.out.println("程序运行结束");
	}
}
           

  【执行结果】主线程休眠三秒才切换标志位值,所以匿名线程中的while循环体可以被执行三次,输出三次执行中,三秒过后,主线程修改标志位值,匿名线程判断到标志位为false,跳出循环体,线程执行结束中止。

运行中
运行中
运行中
程序运行结束
线程中止
           

四、总结

  • stop()

    会清除监视器锁信息,用它来中止线程会导致程序拦腰截断,带来线程安全问题,不建议使用
  • interrupt()

    让处于等待阻塞中的目标线程被中断,根据所处情况的不同会抛出中断异常或返回异常值,除此之外的情况会设置线程的中断状态,不会导致线程安全问题,是一种正确主流的中止线程的方式
  • 标志位

    适用于简单的程序,为了保证线程可见性通常要加上volatile修饰

继续阅读