如何优雅的停止一个线程? (2)

对于线程的停止,最优雅的方式就是通过 interrupt 的方式来实现,关于他的详细文章看之前文章即可,如 InterruptedException 时,再次中断设置,让程序能后续继续进行终止操作。不过对于 interrupt 实现线程的终止在实际开发中发现使用的并不是很多,很多都可能喜欢另一种方式,通过标记位。

用 volatile 标记位的停止方法

关于 volatile 作为标记位的核心就是他的可见性特性,我们通过一个简单代码来看:

/** * @ulr: i-code.online * @author: zhoucx * @time: 2020/9/25 14:45 */ public class MarkThreadTest { //定义标记为 使用 volatile 修饰 private static volatile boolean mark = false; @Test public void markTest(){ new Thread(() -> { //判断标记位来确定是否继续进行 while (!mark){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程执行内容中..."); } }).start(); System.out.println("这是主线程走起..."); try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } //10秒后将标记为设置 true 对线程可见。用volatile 修饰 mark = true; System.out.println("标记位修改为:"+mark); } }

上面代码也是我们之前文中的,这里不再阐述,就是一个设置标记,让线程可见进而终止程序,这里我们需要讨论的是,使用 volatile 是真的都是没问题的,上述场景是没问题,但是在一些特殊场景使用 volatile 时是存在问题的,这也是需要注意的!

volatile 修饰标记位不适用的场景

这里我们使用一个生产/消费的模式来实现一个 Demo

/** * @url: i-code.online * @author: zhoucx * @time: 2020/10/12 10:46 */ public class Producter implements Runnable { //标记是否需要产生数字 public static volatile boolean mark = true; BlockingQueue<Integer> numQueue; public Producter(BlockingQueue numQueue){ this.numQueue = numQueue; } @Override public void run() { int num = 0; try { while (num < 100000 && mark){ //生产数字,加入到队列中 if (num % 50 == 0 ){ System.out.println(num + " 是50的倍数,加入队列"); numQueue.put(num); } num++; } } catch (InterruptedException e) { e.printStackTrace(); }finally { System.out.println("生产者运行结束...."); } } }

首先,声明了一个生产者 Producer,通过 volatile 标记的初始值为 true 的布尔值 mark 来停止线程。而在 run() 方法中,while 的判断语句是 num 是否小于 100000 及 mark 是否被标记。while 循环体中判断 num 如果是 50 的倍数就放到 numQueue 仓库中,numQueue 是生产者与消费者之间进行通信的存储器,当 num 大于 100000 或被通知停止时,会跳出 while 循环并执行 finally 语句块,告诉大家“生产者运行结束”

/** * @url: i-code.online * @author: zhoucx * @time: 2020/10/12 11:03 */ public class Consumer implements Runnable{ BlockingQueue numQueue; public Consumer(BlockingQueue numQueue){ this.numQueue = numQueue; } @Override public void run() { try { while (Math.random() < 0.97){ //进行消费 System.out.println(numQueue.take()+"被消费了...");; TimeUnit.MILLISECONDS.sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("消费者执行结束..."); Producter.mark = false; System.out.println("Producter.mark = "+Producter.mark); } } }


而对于消费者 Consumer,它与生产者共用同一个仓库 numQueue,在 run() 方法中我们通过判断随机数大小来确定是否要继续消费,刚才生产者生产了一些 50 的倍数供消费者使用,消费者是否继续使用数字的判断条件是产生一个随机数并与 0.97 进行比较,大于 0.97 就不再继续使用数字。

/** * @url: i-code.online * @author: zhoucx * @time: 2020/10/12 11:08 */ public class Mian { public static void main(String[] args) { BlockingQueue queue = new LinkedBlockingQueue(10); Producter producter = new Producter(queue); Consumer consumer = new Consumer(queue); Thread thread = new Thread(producter,"producter-Thread"); thread.start(); new Thread(consumer,"COnsumer-Thread").start(); } }

主函数中很简单,创建一个 公共仓库 queue 长度为10,然后传递给两个线程,然后启动两个线程,当我们启动后要注意,我们的消费时有睡眠 100 毫秒,那么这个公共仓库必然会被生产者装满进入阻塞,等待消费。


当消费者不再需要数据,就会将 canceled 的标记位设置为 true,理论上此时生产者会跳出 while 循环,并打印输出“生产者运行结束”。


然而结果却不是我们想象的那样,尽管已经把 Producter.mark设置成 false,但生产者仍然没有停止,这是因为在这种情况下,生产者在执行 numQueue.put(num) 时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断 Producter.mark的值的,所以在这种情况下用 volatile 是没有办法让生产者停下来的,相反如果用 interrupt 语句来中断,即使生产者处于阻塞状态,仍然能够感受到中断信号,并做响应处理。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyyjdp.html