前言
大家好,我是小郭,今天主要来聊一聊如何优雅的停下线程。
在开始之前,我们可以思考一下,如何能够让线程停下?
通过查阅JDK,我们不难发现Thread为我们提供了一个stop方法,只要使用stop方法,就立即停止线程,但是发现stop()方法被标注为废弃的方法,因为这个方法会强行把执行到一半的线程终止,可能会引发一些数据不一致或者我们没发预估的问题。
除了stop()方法,我能想到的方案还有两个,
方案一:使用volatile标记位,利用其可见性
方案二:调用Thread的方法interrupted
方案实现
方案一:使用volatile标记位,利用其可见性
通过代码我们来看下方案一,这是一个很经典的生产者和消费者模式。
生产者Demo
//生产者 class Producer implements Runnable { public volatile boolean canc = false; private Product product; Producer(Product product) { this.product = product; } @Override public void run() { try { while (!canc) { try { //Thread.sleep(1000); product.put("iphone6s"); System.out.println("put:" + Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } } } catch (Exception ex) { ... } finally { System.out.println("结束"); } } }
消费者Demo
//消费者 class Consumer implements Runnable{ private Product product; Consumer(Product product) { this.product = product; } @Override public void run() { while (Math.random() > 0.9){ try { Thread.sleep(1000); product.take("iPhone6s"); System.out.println("take:"+Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } } } }
调用生产者和消费者
public static void main(String[] args) { ArrayBlockingQueue queue = new ArrayBlockingQueue(1); Product product = new Product(queue); Producer producer = new Producer(product); Consumer consumer = new Consumer(product); Thread c1 = new Thread(consumer); Thread p1 = new Thread(producer); p1.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } c1.start(); System.out.println("消费者不需要更多数据了。"); producer.canc = true; System.out.println(producer.canc); System.out.println(p1.getState()); }
场景一:我们把消费者和生产者的线程都开起来,生产者生产一个产品,消费者都会消费一个产品,这个时候volatile的值,在下一次的轮询中值已经变成了true,就跳出while循环,线程就停止,这个场景下volatile就适用了。
场景二:我们将消费者线程不启动,只生产不消费。 理论上我们期待的结果应该也是值变成true,跳出while循环,线程停止。
结果打印:
Put a iphone6s put:Thread-2 消费者不需要更多数据了。 valatile的值: true 线程状态:WAITING
根据打印的结果我们会观察到他没有输出结束的语句,
我们看到了生产者生产了产品,valatile也修改了值,但是线程却没有结束,
这主要的原因是因为,生产者执行了product.put("iphone6s"),没有被消费,造成了阻塞,在它唤醒之前,
无法进入下一次的轮询判断。造成了值修改了,却没有做出相应处理。
我们发现在消费的时候,take方法内部会触发唤醒,当检测到线程已经停止,则抛出InterruptedException异常。
开源码说话,可以看到dequeue,唤醒了线程。
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) notEmpty.await(); return dequeue(); } finally { lock.unlock(); } } public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); } private E dequeue() { ... //释放 notFull.signal(); return x; }
二、方案二:调用Thread的方法interrupted
static class CreateRunable implements Runnable { public CreateRunable(int i) { this.i = i; } private int i; public int getI() { return i; } public void setI(int i) { this.i = i; } @Override public void run() { synchronized (this){ while ( !Thread.currentThread().isInterrupted() ){ System.out.println("Runable接口,实现线程"+i++); } } } }
Thread createThread = new Thread(new CreateRunable(0)); createThread.start(); Thread.sleep(5); createThread.interrupt();
休眠5毫秒后,该线程检查到了中断信号,就会停止线程。
那如果任务正在休眠状态,线程会如何处理呢
@Override public void run() { synchronized (this){ while ( !Thread.currentThread().isInterrupted() ){ try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Runable接口,实现线程"+i++); } } }
抛出异常,同时清除中断状态,线程会继续执行
Runable接口,实现线程0 java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at main.Thread.threadStartThreeWays$CreateRunable.run(threadStartThreeWays.java:48) at java.lang.Thread.run(Thread.java:748) Runable接口,实现线程1 Runable接口,实现线程2 Runable接口,实现线程3
总结
我们在这里就不说Stop()方法,因为他太暴力了,不够优雅。这里的优雅指的是可以让线程有时间做好收尾工作,避免数据的错乱。 优雅停下线程的方式主要有两种
- 方案一:使用volatile标记位。
- 方案二:调用Thread的方法interrupted。
通过上面的demo案例,我们可以看到使用方案一的volatile,在某一些特殊的场景下,会发生不能关闭线程的情况。
所以volatile是不够全面的。方案二则是一种更优的选择。