一、实现线程只有一种方式!
实现线程是并发编程中的基础,必须要先实现多线程,才可以继续后续的一系列的操作。而实现线程的的方式到底有几种?
- 2种?
- 3种?
- 4种?
1.1、实现线程的x种方式?
第一种:通过实现Runnable接口的方式实现多线程
- 首先自定义类实现
Runnable接口 - 重新
run()方法 - 把自定义类的实例传到Thread类中即可实现多线程
package com.strivelearn.concurrent.chapter01;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
/**
* @author strivelearn
* @version MyThread.java, 2022年12月16日
*/
@Slf4j
public class MyImplThread implements Runnable {
@SneakyThrows
@Override
public void run() {
log.info("MyImplThread多线程:{}执行逻辑中...", Thread.currentThread().getName());
Thread.sleep(5000);
log.info("MyImplThread多线程:{}通过实现Runnable接口来实现多线程", Thread.currentThread().getName());
log.info("MyImplThread多线程:{}执行结束", Thread.currentThread().getName());
}
}
package com.strivelearn.concurrent.chapter01;
import lombok.extern.slf4j.Slf4j;
/**
* @author strivelearn
* @version TestMain.java, 2022年12月16日
*/
@Slf4j
public class TestMain {
public static void main(String[] args) {
log.info("我是:{}线程,开始执行", Thread.currentThread().getName());
Thread myThread = new Thread(new MyImplThread());
myThread.start();
log.info("我是:{}线程,执行结束", Thread.currentThread().getName());
}
}
2022-12-16 20:17:56.263 [main] INFO com.strivelearn.concurrent.chapter01.TestMain - 我是:main线程,开始执行
2022-12-16 20:17:56.266 [main] INFO com.strivelearn.concurrent.chapter01.TestMain - 我是:main线程,执行结束
2022-12-16 20:17:56.266 [Thread-0] INFO com.strivelearn.concurrent.chapter01.MyImplThread - MyImplThread多线程:Thread-0执行逻辑中...
2022-12-16 20:18:01.268 [Thread-0] INFO com.strivelearn.concurrent.chapter01.MyImplThread - MyImplThread多线程:Thread-0通过实现Runnable接口来实现多线程
2022-12-16 20:18:01.268 [Thread-0] INFO com.strivelearn.concurrent.chapter01.MyImplThread - MyImplThread多线程:Thread-0执行结束
第二种方式:继承Thread类来实现多线程
- 与第一种方式不同的是它没有实现接口,而是继承Thread类,并重写了
run()方法
package com.strivelearn.concurrent.chapter01;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
/**
* @author strivelearn
* @version MyExtendThread.java, 2022年12月16日
*/
@Slf4j
public class MyExtendThread extends Thread {
@SneakyThrows
@Override
public void run() {
log.info("MyExtendThread多线程:{}执行逻辑中...", Thread.currentThread().getName());
Thread.sleep(5000);
log.info("MyExtendThread多线程:{}通过实现Runnable接口来实现多线程", Thread.currentThread().getName());
log.info("MyExtendThread多线程:{}执行结束", Thread.currentThread().getName());
}
}
package com.strivelearn.concurrent.chapter01;
import lombok.extern.slf4j.Slf4j;
/**
* @author strivelearn
* @version TestMain.java, 2022年12月16日
*/
@Slf4j
public class TestMain {
public static void main(String[] args) {
log.info("我是:{}线程,开始执行", Thread.currentThread().getName());
// Thread myThread = new Thread(new MyImplThread());
Thread myThread = new MyExtendThread();
myThread.start();
log.info("我是:{}线程,执行结束", Thread.currentThread().getName());
}
}
2022-12-16 20:18:40.266 [main] INFO com.strivelearn.concurrent.chapter01.TestMain - 我是:main线程,开始执行
2022-12-16 20:18:40.268 [main] INFO com.strivelearn.concurrent.chapter01.TestMain - 我是:main线程,执行结束
2022-12-16 20:18:40.268 [Thread-0] INFO c.strivelearn.concurrent.chapter01.MyExtendThread - MyExtendThread多线程:Thread-0执行逻辑中...
2022-12-16 20:18:45.270 [Thread-0] INFO c.strivelearn.concurrent.chapter01.MyExtendThread - MyExtendThread多线程:Thread-0通过实现Runnable接口来实现多线程
2022-12-16 20:18:45.272 [Thread-0] INFO c.strivelearn.concurrent.chapter01.MyExtendThread - MyExtendThread多线程:Thread-0执行结束
第三种:通过线程池来实现多线程
- 默认采用DefaultThreadFactory
- 它会给我们线程池创建的线程设置一些默认的值,比如它的名字,它是不是守护线程,以及它的优先级
package com.strivelearn.concurrent.chapter01;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author strivelearn
* @version TestMain.java, 2022年12月16日
*/
@Slf4j
public class TestMain {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 6, TimeUnit.MINUTES, new ArrayBlockingQueue<>(20));
executor.execute(() -> System.out.println("我是" + Thread.currentThread().getName() + "执行的"));
executor.shutdown();
}
}
我是pool-2-thread-1执行的
但是线程池这种创建线程的方式,还是通过new Thread()来实现的
源码:
ThreadPoolExecutor 930行
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
第四种:有返回值的callable也是一种新建多线程的方式
- 实现callable接口,并且给它的泛型设置成integer,然后会返回一个随机数
package com.strivelearn.concurrent.chapter01;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
import java.util.concurrent.Callable;
/**
* @author strivelearn
* @version MyCallableThread.java, 2022年12月16日
*/
@Slf4j
public class MyCallableThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int i = new Random().nextInt();
log.info("MyCallableThread返回了随机数:{}", i);
return i;
}
}
package com.strivelearn.concurrent.chapter01;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
/**
* @author strivelearn
* @version TestMain.java, 2022年12月16日
*/
@Slf4j
public class TestMain {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
Future<Integer> submit = executorService.submit(new MyCallableThread());
log.info("获取MyCallableThread返回的值:{}",submit.get());
}
}
2022-12-16 20:58:29.766 [pool-2-thread-1] INFO c.s.concurrent.chapter01.MyCallableThread - MyCallableThread返回了随机数:-1926905950
2022-12-16 20:58:29.768 [main] INFO com.strivelearn.concurrent.chapter01.TestMain - 获取MyCallableThread返回的值:-1926905950
第五种:Timer定时器
public abstract class TimerTask implements Runnable {
/**
* This object is used to control access to the TimerTask internals.
*/
final Object lock = new Object();
TimerTask实现了Runnable接口了
public class Timer {
/**
* The timer task queue. This data structure is shared with the timer
* thread. The timer produces tasks, via its various schedule calls,
* and the timer thread consumes, executing timer tasks as appropriate,
* and removing them from the queue when they're obsolete.
*/
private final TaskQueue queue = new TaskQueue();
/**
* The timer thread.
*/
private final TimerThread thread = new TimerThread(queue);
Timer内部有个TimerThread继承Thread,本质上还是Thread
第六种:匿名类内部类
第七种:Lambda表达式
二、创建线程的本质
通过上面透过现象看本质,它们最终实现都是实现runnable接口或者继承Thread类
但是这两种其实是一种方式:
两种方式的最主要的区别在与run()方法的内容的来源
Thread类的run方法
@Override
public void run() {
if (target != null) {
target.run();
}
}
- 实现runnable接口,把Runnable实例作为target对象,传给Thread类,最终调用target.run()
- 继承thread类,重写Thread的run()方法,Thread.start()会执行run()方法
所以:创建线程的只有一种方式:构造Thread类
2.1、实现Runnable接口要比继承Thread类要好
- 可以把不同的内容进行解耦,权责分明
- 某些情况下可以提升性能,减小开销
- 继承Thread类相当于限制了代码未来的可扩展性(Java不支持双继承)
三、如何正确的停止线程?
3.1、如何正确停止一个线程?
启动线程需要调用Thread类的start()方法,并在run()方法中定义需要执行的任务。启动一个线程非常简单,但是想要正确停止线程就没那么容易了。
-
通常情况下,我们不会手动停止一个线程,而是允许线程运行到整个进程结束,然后
让它自然停止 -
但是依然有特殊的情况需要
我们手动停止线程- 用户突然关闭程序
- 程序运行出错重启
- ...
这时候,即将停止的线程在很多业务场景下仍然很有价值,但是Java并没有提供简单易用,能够直接安全停止线程的能力。
对于Java而言,最正确的停止线程的方式是使用interrupt,但是interrupt仅仅起到通知被停止线程的作用。(只是通知的作用)
package com.strivelearn.concurrent.chapter01.interrupt;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
/**
* @author strivelearn
* @version ThreadInterrupt.java, 2022年12月17日
*/
@Slf4j
public class ThreadInterrupt {
@SneakyThrows
public static void main(String[] args) {
Thread stopThread = new Thread(new RightStopThread());
stopThread.start();
// 休眠5毫秒
Thread.sleep(5);
// 开始打断子线程的执行
stopThread.interrupt();
log.info("ThreadInterruptWithSleep的状态标记位:" + stopThread.isInterrupted());
}
static class RightStopThread implements Runnable {
@SneakyThrows
@Override
public void run() {
int count = 0;
// 当前线程没有被通知停止,理论情况下一直无线打印下去
while (!Thread.currentThread().isInterrupted()) {
log.info("count={}", count++);
}
log.info("RightStopThread已停止");
}
}
}
2022-12-17 15:46:20.006 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=0
2022-12-17 15:46:20.009 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=1
2022-12-17 15:46:20.009 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=2
2022-12-17 15:46:20.009 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=3
2022-12-17 15:46:20.009 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=4
2022-12-17 15:46:20.009 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=5
2022-12-17 15:46:20.009 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=6
2022-12-17 15:46:20.009 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=7
2022-12-17 15:46:20.009 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=8
2022-12-17 15:46:20.009 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=9
2022-12-17 15:46:20.009 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=10
2022-12-17 15:46:20.009 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=11
2022-12-17 15:46:20.009 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=12
2022-12-17 15:46:20.010 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=13
2022-12-17 15:46:20.010 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=14
2022-12-17 15:46:20.010 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=15
2022-12-17 15:46:20.010 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=16
2022-12-17 15:46:20.010 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=17
2022-12-17 15:46:20.010 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=18
2022-12-17 15:46:20.010 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=19
2022-12-17 15:46:20.010 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=20
2022-12-17 15:46:20.010 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=21
2022-12-17 15:46:20.010 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - count=22
2022-12-17 15:46:20.010 [Thread-0] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - RightStopThread已停止
2022-12-17 15:46:20.010 [main] INFO c.s.concurrent.chapter01.interrupt.ThreadInterrupt - ThreadInterruptWithSleep的状态标记位:true
3.2、线程sleep期间能否感受到中断
比如在RightStopThread线程作业中,进行Thread.sleep。那么能否感受到线程中断了??
当在一个被阻塞的线程(调用sleep或者wait等 会让线程的阻塞的方法)上调用interrupt方法时,阻塞调用将会被Interrupted Exception异常中断,将中断标记位设置为false
package com.strivelearn.concurrent.chapter01.interrupt;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
/**
* @author strivelearn
* @version ThreadInterrupt.java, 2022年12月17日
*/
@Slf4j
public class ThreadInterruptWithSleep {
@SneakyThrows
public static void main(String[] args) {
Thread thread = new Thread(new ThreadInterruptWithSleep()::stopThread);
thread.start();
Thread.sleep(5);
thread.interrupt();
log.info("ThreadInterruptWithSleep的状态标记位:" + thread.isInterrupted());
}
void stopThread() {
int count = 0;
// 当前线程没有被通知停止,理论情况下一直无线打印下去
while (!Thread.currentThread().isInterrupted()) {
log.info("count={}", count++);
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.info("RightStopThread已停止");
}
}
2022-12-17 15:46:59.906 [Thread-0] INFO c.s.c.chapter01.interrupt.ThreadInterruptWithSleep - count=0
2022-12-17 15:46:59.910 [main] INFO c.s.c.chapter01.interrupt.ThreadInterruptWithSleep - ThreadInterruptWithSleep的状态标记位:false
Exception in thread "Thread-0" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
at com.strivelearn.concurrent.chapter01.interrupt.ThreadInterruptWithSleep.stopThread(ThreadInterruptWithSleep.java:29)
at java.lang.Thread.run(Thread.java:750)
Caused by: java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.strivelearn.concurrent.chapter01.interrupt.ThreadInterruptWithSleep.stopThread(ThreadInterruptWithSleep.java:27)
... 1 more
注意:这边的状态为false。也就是sleep把状态标志位设置了false
最佳处理方式
-
在方法签名中抛出异常,run()强制try...catch
- 使用throw InterruptedException标记你的方法,不采用try语句块捕获异常,以便于该异常可以传递到顶层,让run方法可以捕获这个异常
- 由于run方法内无法抛出checked Exception(只能用try catch),所以方法上抛出异常的好处是,顶层方法必须处理该异常,不够漏掉或者被吞掉
-
可以再次中断线程
- 在catch语句块中调用Thread.currentThread().interrupt()函数
- 因为如果线程在休眠期间被中断,那么会自动清楚中断信号
- 如果这个时候添加中断信息,中断信号依然可以被捕获到
- 这样后续执行的方法依然可以检测出这里发生过中断,可以做出相应的处理,整个线程可以正常退出
package com.strivelearn.concurrent.chapter01.interrupt;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
/**
* @author xys
* @version ThreadInterrupt.java, 2022年12月17日
*/
@Slf4j
public class ThreadInterruptProcess {
@SneakyThrows
public static void main(String[] args) {
Thread stopThread = new Thread(new RightStopThread());
stopThread.start();
// 休眠5毫秒
Thread.sleep(5);
// 开始打断子线程的执行
stopThread.interrupt();
log.info("ThreadInterruptWithSleep的状态标记位:" + stopThread.isInterrupted());
}
static class RightStopThread implements Runnable {
@SneakyThrows
@Override
public void run() {
int count = 0;
// 当前线程没有被通知停止,理论情况下一直无线打印下去
while (!Thread.currentThread().isInterrupted()) {
log.info("count={}", count++);
calc();
}
log.info("RightStopThread已停止");
}
}
static void calc(){
log.error("计算逻辑");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
}
calc方法吃掉了InterruptedException,导致状态位失效,上面while一直是处理。所以下面才是正确的处理方式
package com.strivelearn.concurrent.chapter01.interrupt;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
/**
* @author xys
* @version ThreadInterrupt.java, 2022年12月17日
*/
@Slf4j
public class ThreadInterruptProcess {
@SneakyThrows
public static void main(String[] args) {
Thread stopThread = new Thread(new RightStopThread());
stopThread.start();
// 休眠5毫秒
Thread.sleep(5);
// 开始打断子线程的执行
stopThread.interrupt();
log.info("ThreadInterruptWithSleep的状态标记位:" + stopThread.isInterrupted());
}
static class RightStopThread implements Runnable {
@Override
public void run() {
int count = 0;
// 当前线程没有被通知停止,理论情况下一直无线打印下去
while (!Thread.currentThread().isInterrupted()) {
log.info("count={}", count++);
try {
calc();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
log.info("RightStopThread已停止");
}
}
static void calc() throws InterruptedException {
log.info("当前执行线程:" + Thread.currentThread().getName());
log.error("计算逻辑");
Thread.sleep(500);
}
}
2022-12-17 16:23:35.098 [Thread-0] INFO c.s.c.chapter01.interrupt.ThreadInterruptProcess - count=0
2022-12-17 16:23:35.101 [Thread-0] INFO c.s.c.chapter01.interrupt.ThreadInterruptProcess - 当前执行线程:Thread-0
2022-12-17 16:23:35.101 [Thread-0] ERROR c.s.c.chapter01.interrupt.ThreadInterruptProcess - 计算逻辑
2022-12-17 16:23:35.103 [main] INFO c.s.c.chapter01.interrupt.ThreadInterruptProcess - ThreadInterruptWithSleep的状态标记位:true
2022-12-17 16:23:35.103 [Thread-0] INFO c.s.c.chapter01.interrupt.ThreadInterruptProcess - RightStopThread已停止
3.3、错误的停止线程的方法
-
stop,会直接把线程停止,导致任务戛然而止,有风险
-
suspend(暂停)、resume(恢复)组合 容易导致死锁
如果线程调用suspend,这个线程并不会释放锁,就开始进入休眠,此时会持有锁,容易导致死锁问题。比如线程a调用了suspend方法让线程b挂起,b进入休眠,而线程b恰好持有这个锁,此时线程a想访问线程b持有的那把锁,而线程b并没有释放那把锁。这样就造成了死锁问题
以前2种都是被jdk设置了废弃了
package com.strivelearn.concurrent.chapter01.deadlock;
import java.util.Objects;
/**
* @author strivelearn
* @version DealLockMain.java, 2022年12月17日
*/
public class DealLockMain {
public static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(DealLockMain::calc);
Thread t2 = new Thread(DealLockMain::calc);
t1.start();
t2.start();
t1.suspend();
}
static void calc() {
synchronized (lock) {
System.out.println("当前线程:" + Thread.currentThread().getName() + "开始拿到lock锁开始计算");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
当前线程:Thread-1开始拿到lock锁开始计算
上述代码一直在等待lock锁的释放
3.4、为什么用volatile标记位的停止方法是错误的
package com.strivelearn.concurrent.chapter01.volatilestop;
/**
* @author strivelearn
* @version VolatileStopMain.java, 2022年12月17日
*/
public class VolatileStopMain {
public static class VolatileThread implements Runnable {
private volatile boolean canceled = false;
@Override
public void run() {
int count = 0;
while (!canceled) {
System.out.println("count: " + count++);
}
}
public static void main(String[] args) throws InterruptedException {
VolatileThread volatileThread = new VolatileThread();
Thread thread = new Thread(volatileThread);
thread.start();
Thread.sleep(10);
volatileThread.canceled = true;
}
}
}
这种情况下,volatile修饰的标记位可以正常使用。但是volatile不仅仅在这种情况能正确停止线程,其他情况下,也应该停止线程,但是没达到效果
比如BlockingQueue的put\take方法,都是阻塞的
package com.strivelearn.concurrent.chapter01.volatilestop;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingDeque;
import java.util.function.Consumer;
/**
* @author strivelearn
* @version VolatileStopMain.java, 2022年12月17日
*/
public class VolatileStopMain2 {
private static ArrayBlockingQueue storage = new ArrayBlockingQueue(8);
private static volatile boolean canceled = false;
public static class Producer implements Runnable {
@Override
public void run() {
int num = 0;
try {
while (!canceled) {
try {
// put是阻塞的
// 把值加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
storage.put(num++);
System.out.println("生产" + num);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
} finally {
System.out.println("生产者结束运行");
}
}
}
static class Consumer implements Runnable {
@Override
public void run() {
try {
// 需要消费
boolean needNum = true;
while (needNum) {
try {
// take是阻塞的
// take取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止
int num = (int) storage.take();
// 代表消费慢点
Thread.sleep(100);
System.out.println("消费" + num);
// 当消费到6的时候不需要消费了。
if (num == 6) {
needNum = false;
// 一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况却停不下来
canceled = true;
System.out.println("当前仓库的数量:" + storage.size());
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
} finally {
System.out.println("消费者运行结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread producerThread = new Thread(new Producer());
producerThread.start();
Thread consumerThread = new Thread(new Consumer());
consumerThread.start();
}
}
生产2
生产3
生产4
生产5
生产6
生产7
生产8
生产9
消费0
生产10
消费1
生产11
消费2
生产12
消费3
生产13
消费4
生产14
消费5
生产15
消费6
当前仓库的数量:8
消费者运行结束
可以看到生产者并没有运行结束。 因为仓库满了之后,put会让线程进行阻塞,线程阻塞之后,它在被唤醒前是没办法进入到下一次循环中,所以生产者始终在storage.put(num++);这个语句这边,即便我们把canceled置为true、还是false。当前线程是没办法感知的。
所以如果用interrupt去处理,即便线程在休眠,也会感应到,进行线程中断