三线程停止、中断之最佳实践
1.停止线程的正确方法(原理)
通常线程会在什么情况下停止
正常运行的线程只有在两种状况下会停止
1.run方法的代码全部执行完毕,正确结束的情况
2.出现异常,并且方法没有捕获,这时线程也会停止
线程停止之后,它所占有的资源比如说内存等,都会和其他不可达的对象一样被jvm回收
通过interrupt来通知,而不是强制停止
注意只是通知,如果被通知线程执行的代码没有对这个通知进行处理,没有在被捕获的异常中做相应的退出操作,那么是不会真正退出的,而是直接执行到run方法代码结束为止
之所以如此设计是因为被通知的线程自己最清楚应该在何处退出,应不应该退出
正确停止的好处
这样可以确保线程是从预期的位置开始退出(在catch (InterruptedException e){}代码块中设置退出相关代码或在if (Thread.currentThread().isInterrupted())处退出),而强制停止将无法把握程序的状态,无法确认线程到底会在执行到那些代码的时候停止.这将可能导致一些未知的错误.
实现原理
每个线程都有自己的interrupted state(中断状态)标志位,通过调用对应线程的interrupt()来通知,可以修改这个标志位,如果线程在运行过程中被调用了interrupt方法,这个线程的标志位会为true.这时,通过检查标志位,可以直接退出,或者手动抛出一个interruptException
//线程安全问题,发生于线程间的共有变量,线程单独私有的变量是不会发生安全问题的.
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
int i=0;
for(;;) {
i++;
//if (Thread.currentThread().isInterrupted())return;
if (Thread.currentThread().isInterrupted()) throw new InterruptedException();
if(i%500000000==0) System.out.println(i);
}
} catch (InterruptedException e) {
System.err.println("中断退出");
return;
}
});
thread.start();
Thread.sleep(100);
thread.interrupt();
}
跟如下情况一起处理然后退出
//java.lang.Thread#interrupt
//被调用interrupt方法的线程
If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.
如果该线程在调用Object类的wait()、wait(long)或wait(long,int)方法,
或调用该类的join()、join(long)、join(long,int)、sleep(long)或sleep(long,int)方法时被阻塞,
那么它的中断状态将被清除,并收到一个InterruptedException。
2.最佳实践
普通情况,没有调用线程相关的阻塞方法
/**
* run方法中没有线程相关阻塞方法的情况下退出例如wait或sleep
*/
public class RightWayReturnThreadWithoutBlock {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int num = 0;
for (; ; ) {
if (!Thread.currentThread().isInterrupted()&&num++ < Integer.MAX_VALUE / 2) {
if (num % 10000 == 0)
System.out.println(Thread.currentThread().isInterrupted()+"是一万的倍数>>>" + num);
}
else return;
}
});
//注意不能在线程没开始前就设置中断状态,这是无效的
//因为start之后默认会设为false
//thread.interrupt();
thread.start();
Thread.sleep(1000);
thread.interrupt();
System.err.println(thread.isInterrupted());
}
}
调用相关阻塞方法
/**
* run方法中有线程相关阻塞方法的情况下退出例如wait或sleep
*/
public class RightWayReturnThreadWithBlock {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int num = 0;
try {
for (; ; ) {
if (!Thread.currentThread().isInterrupted()) {
num++;
System.out.println(Thread.currentThread().isInterrupted() + "当前数字>>>" + num);
} else {
num = 100 - 0;
System.out.println("error return,done num "+num);
return;
}
if (num > 100) {
System.out.println("wait other work");
Thread.currentThread().sleep(2000);
return;
}
}
} catch (InterruptedException e) {
System.err.println("other work is ok");
System.err.println("continue next task");
}
});
thread.start();
Thread.sleep(1000);
thread.interrupt();
System.err.println(thread.isInterrupted());
}
}
结果
false当前数字>>>1
...
false当前数字>>>100
false当前数字>>>101
wait other work
false
other work is ok
continue next task
每次循环都阻塞
代码
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
System.out.print(Thread.currentThread().isInterrupted() + ">>>");
System.out.println(LocalTime.now().format(DateTimeFormatter.ISO_TIME));
} catch (InterruptedException e) {
System.out.print(Thread.currentThread().isInterrupted() + ">>>");
e.printStackTrace();
}
}
});
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
结果
false>>>22:21:14.757
false>>>22:21:15.777
false>>>22:21:16.78
false>>>22:21:17.784
false>>>java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.example.threadCoreKnowledge.stopThread_3.RightWayReturnThreadWithPerIterationBlock.lambda$main$0(RightWayReturnThreadWithPerIterationBlock.java:14)
at java.lang.Thread.run(Thread.java:748)
false>>>22:21:19.762
false>>>22:21:20.765
可以看到报了一个错误,这也就意味着可以通过在catch代码块中进行退出(return)操作,而不用通过Thread.currentThread.isInterrupted每次判断了.
首先之所以会触发中断报错且之后中断状态依旧是false是因为
/*If this thread is blocked in an invocation of the wait(), wait(long), *or wait(long, int) methods of the Object class, or of the join(), *join(long), join(long, int), sleep(long), or sleep(long, int), methods *of this class, then its interrupt status will be cleared and it will *receive an InterruptedException.*/
//then its interrupt status will be cleared and it will receive an InterruptedException
java.lang.Thread#interrupt
public void interrupt()
同时也可以看到,虽然报错了,但是因为错误被捕获且没有相应的退出操作,所以程序依旧继续运行.
这也印证了上面所说的interrupt()只是进行通知而不直接干涉.而在上个段落***调用相关阻塞方法***的代码中,之所以可以直接退出,也是验证了一开所说的***(通常线程会在什么情况下停止)的第一种情况,即run方法执行结束***
而之所以设计为每次报错都重置标志位,也是因为代表通知已经给到了catch块中的内容也执行过了,但是有些程序可能因为没有满足条件所以没有退出,但是之后还是有可能会退出的.为了这种扩展性,所以需要复位,可以再次进入catch块和再次从false变为true,这样就可以有这种当某次interrupt()调用且满足退出条件后退出的更复杂的情况.
以下为根据interrupt的异步特性通过密集调用假设不复位的情况
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
new Random().ints().limit(5).forEach(it -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(e.getClass().getSimpleName()
+ ":" + e.getMessage());
}
});
});
thread.start();
System.out.println("01"+thread.isInterrupted());
thread.interrupt();
System.out.println("02"+thread.isInterrupted());
thread.interrupt();
System.out.println("03"+thread.isInterrupted());
thread.interrupt();
System.out.println("04"+thread.isInterrupted());
thread.interrupt();
System.out.println("05"+thread.isInterrupted());
Thread.sleep(2000);
//interrupt()是通知jvm去设置标记位,不代表立马生效
//sync无法取消指令重排,但可以确保代码块在同一时刻只有一个线程访问
//而JVM保证了对于"一个代码块在同一时刻只有一个线程访问"的情况,会
//维护一种类似串行的语义.即程序执行结果与在严格串行环境中执行结果相同,
//那么指令的重排序就是允许的.
synchronized (Class.class) {
thread.interrupt();
System.out.println("1"+thread.isInterrupted());
System.out.println("2"+thread.isInterrupted());
System.out.println("3"+thread.isInterrupted());
}
}
测试结果1
01false
02true
03true
04true
05true
InterruptedException:sleep interrupted
1true
2false
InterruptedException:sleep interrupted
3false
测试结果2
01false
02true
03true
04true
05true
InterruptedException:sleep interrupted
1false
InterruptedException:sleep interrupted
2false
3false
测试结果3
01false
02true
03true
04true
05true
InterruptedException:sleep interrupted
1true
InterruptedException:sleep interrupted
2false
3false
可以看到,如果不复位,之后再执行thread.interrupt();通知的作用就相当于失效了.
开发中的两种最佳实践
1.优先选择:传递中断
需要在子方法的签名上抛出异常,统一在run中处理.而run方法,根据实现接口方法签名的限制,实现类不能抛出超出被实现/重写的抽象方法/非抽象方法的异常,所以如果子方法抛出异常,一定需要在run方法中处理.
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
try {
throw0();
// throw1();
//确保语法上可以catch InterruptedException
if (false) throw new InterruptedException();
} catch (InterruptedException e) {
System.err.println("return");
return;
} catch (Exception e) {
}
}
});
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
//抛出异常
public static void throw0() throws InterruptedException {
Thread.sleep(2000);
}
//自己catch住
public static void throw1() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.err.println("throw1>>>"+e.getClass().getName());
return;
}
}
一.抛出异常throw0();结果
return
程序直接停止,满足预期.
二.自己catch住throw1();结果
throw1>>>java.lang.InterruptedException
并没有停止,只退出了throw1的方法栈帧,没有退出run的方法栈帧,程序依旧在不断的循环中,破坏了run方法catch代码块的语义.
2.不想或无法传递:恢复中断
当前也需要根据这个异常依赖当前方法的变量做一些处理
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
try {
throw2();
//确保语法上可以catch InterruptedException
if (false) throw new InterruptedException();
} catch (InterruptedException e) {
System.err.println("return");
return;
} catch (Exception e) {
}
}
});
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
//自己catch住,但依旧上报
public static void throw2() throws InterruptedException {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.err.println("throw2>>>" + e.getClass().getName());
throw new InterruptedException();
}
}
结果
throw2>>>java.lang.InterruptedException
return
如果遇到子方法需要处理一些异常,同时遭遇这些异常也要中断的情况
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
try {
throw2();
//确保语法上可以catch InterruptedException
if (false) throw new InterruptedException();
} catch (InterruptedException e) {
System.err.println("return");
Arrays.stream(e.getSuppressed())
.map(it->(Exception)it)
.forEach(System.out::println);
return;
} catch (Exception e) {
}
}
});
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
//自己catch住依旧上报且遭遇其它一些异常时也退出.
public static void throw2() throws InterruptedException {
try {
System.out.println(1 / 0);
Thread.sleep(2000);
} catch (InterruptedException e) {
System.err.println("throw2>>>" + e.getClass().getName());
throw new InterruptedException();
} catch (ArithmeticException e) {
InterruptedException exception = new InterruptedException();
exception.addSuppressed(e);
throw exception;
}
}
结果
return
java.lang.ArithmeticException: / by zero
通过恢复中断的方式,在run方法中结束语句
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true&&!Thread.currentThread().isInterrupted()) {
throw3();
}
});
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
//自己catch住,同时恢复中断
public static void throw3() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.err.println("throw3>>>" + e.getClass().getName());
//恢复中断
Thread.currentThread().interrupt();
}
}
结果
throw3>>>java.lang.InterruptedException
3.不应屏蔽中断
对于run方法栈帧的的中断退出,应该由run方法自己决定,余下的其它方法栈帧不应该在不上报的情况下处理这个InterruptException.
不然既无法中断破坏了语义,又因为程序还在运行日志也在继续记录导致日志也难以排除
3.响应线程中断的N种方法和情况
代表方法在处理过程中可以感知到interrupt的中断信号.
/**
Interrupts this thread.
Unless the current thread is interrupting itself, which is always permitted, the checkAccess method of this thread is invoked, which may cause a SecurityException to be thrown.
If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.
If this thread is blocked in an I/O operation upon an InterruptibleChannel then the channel will be closed, the thread's interrupt status will be set, and the thread will receive a java.nio.channels.ClosedByInterruptException.
If this thread is blocked in a java.nio.channels.Selector then the thread's interrupt status will be set and it will return immediately from the selection operation, possibly with a non-zero value, just as if the selector's wakeup method were invoked.
If none of the previous conditions hold then this thread's interrupt status will be set.
Interrupting a thread that is not alive need not have any effect.
Throws:
SecurityException – if the current thread cannot modify this thread
*/
java.lang.Thread#interrupt
public void interrupt() {
java.lang.Object wait(), wait(long), or wait(long, int)
java.lang.Thread join(), join(long), join(long, int), sleep(long), or sleep(long, int)
java.nio.channels.InterruptibleChannel相关方法
throws java.nio.channels.ClosedByInterruptException.
java.nio.channels.Selector的相关方法
java.util.concurrent.BlockingQueue poll(long timeout, TimeUnit unit),take(),put(E e),offer(E e, long timeout, TimeUnit unit)
java.util.concurrent.locks.Lock lockInterruptibly(),tryLock(long time, TimeUnit unit)
java.util.concurrent.CountDownLatch await(),await(long timeout, TimeUnit unit)
java.util.concurrent.CyclicBarrier await(),dowait(boolean timed, long nanos),await(long timeout, TimeUnit unit)
java.util.concurrent.Exchanger exchange(V),exchange(V x, long timeout, TimeUnit unit)
4.错误的停止方法
Thread自带的强制方法
Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?
synchronize(Object)块的进出的原子性就是通过持有Object对应的monitor来保证的,进入代码块时,线程会持有这个monitor,其它线程无法获取这个monitor就会被传入等待队列挂起(JVM实现),退出后解除对这个monitor的持有,这时队列中的某个线程就会因为争抢到这个monitor而被运行进入synchronize代码块,所以可以保证一次只有一个线程运行这块代码
stop:
这个方法本质上是不安全的。
用Thread.stop停止一个线程会导致它解锁所有被锁定的监控器(monitors)(这是未被选中的ThreadDeath异常在堆栈上传播的自然结果)。
如果之前被这些监控器(monitors)保护的对象中有任何一个处于不一致的状态,那么受损的对象就会对其他线程可见,例如单例模式中在synchronize块中已经初始化了一些信息来创建了一个对象但还没有让其被正确引用的时候,突然stop了,其他线程又可以获得这个锁进来,根据判断发现还没有初始化,所以重复初始化和创建对象,导致本不该执行多次的方法执行多次,且还有可能导致其它任意行为。
这些行为可能很微妙且难以检测,也可能很明显.且与其它未经处理的Exception不同,ThreadDeath会默默的杀死线程不会有一个异常的抛出.因此,用户没法警告程序已经被破坏.这种导致的错误可能在实际错误发生后的任何时间被察觉,可能是立刻,也可能是几小时,甚至是几天后.
stop的许多用法都应该被简单修改一些变量的代码所取代,以表明目标线程应该停止运行。
目标线程应该定期检查这个变量,如果变量指示它要停止运行,就应该有序地从它的run方法中返回。如果目标线程长时间等待(例如在条件变量上),应该使用中断(interrupt)方法来中断等待。
程序的突然停止,可能导致未知的错误,和程序执行结果的不确定.
/**
* 模拟给连队发放武器的方式,展示stop的危害
*/
public class ThreadStop {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
//5个连队
for (int i = 0; i < 5; i++) {
System.out.println("连队>>>" + (i+1));
AtomicInteger index = new AtomicInteger(1);
int index0=i;
//通过随机数模拟程序运行时不确定的耗时
new Random().ints(5, 0, 50)
.forEach(time -> {
System.out.print("连队>>>" + (index0 + 1));
System.out.println("第>>>" + (index.getAndIncrement()) + "人领取到了");
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("连队>>>" + (index0 + 1) + "领取完毕");
}
});
thread.start();
//等待一会让线程启动运行
Thread.sleep(200);
thread.stop();
}
}
测试结果1
连队>>>1
连队>>>1第>>>1人领取到了
连队>>>1第>>>2人领取到了
连队>>>1第>>>3人领取到了
连队>>>1第>>>4人领取到了
连队>>>1第>>>5人领取到了
连队>>>1领取完毕
连队>>>2
连队>>>2第>>>1人领取到了
连队>>>2第>>>2人领取到了
连队>>>2第>>>3人领取到了
测试结果2
连队>>>1
连队>>>1第>>>1人领取到了
连队>>>1第>>>2人领取到了
连队>>>1第>>>3人领取到了
连队>>>1第>>>4人领取到了
连队>>>1第>>>5人领取到了
连队>>>1领取完毕
连队>>>2
连队>>>2第>>>1人领取到了
连队>>>2第>>>2人领取到了
连队>>>2第>>>3人领取到了
连队>>>2第>>>4人领取到了
连队>>>2第>>>5人领取到了
连队>>>2领取完毕
连队>>>3
连队>>>3第>>>1人领取到了
连队>>>3第>>>2人领取到了
测试结果3
连队>>>1
连队>>>1第>>>1人领取到了
连队>>>1第>>>2人领取到了
连队>>>1第>>>3人领取到了
连队>>>1第>>>4人领取到了
连队>>>1第>>>5人领取到了
连队>>>1领取完毕
连队>>>2
连队>>>2第>>>1人领取到了
resume:
此方法仅用于与suspend一起使用,suspend已被弃用,因为它容易发生死锁。
suspend:
这个方法已经被废弃,因为它本身就容易产生死锁。如果目标线程在暂停时持有保护关键系统资源的监视器(monitor)的锁,那么在目标线程恢复之前,任何线程都不能访问这个资源。如果将恢复目标线程的线程在调用resume(恢复)之前需要试图获得这个监视器,就会产生死锁。这种死锁通常表现为 “冻结”(“frozen”) 进程。
被弃用是因为容易产生死锁,当它被暂停的时候,如果本身持有锁,那么这个锁不会归还,而是一直持有.这也就导致如果调用恢复这个被暂停线程的方法(resume)之前需要持有这个锁,那将无法获得这个锁,导致调用恢复方法的线程进入等待队列.且所有需要这个monitor的锁的线程会全部死锁.
通过volatile设置boolean标记位
volatile的局限
适用的情况
private volatile boolean sign = true;
public static void main(String[] args) throws InterruptedException {
WrongWayVolatileSignStop stop = new WrongWayVolatileSignStop();
new Thread(() -> {
while (stop.sign) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(LocalTime.now().format(DateTimeFormatter.ISO_TIME));
}
}).start();
Thread.sleep(1000);
stop.sign = false;
}
结果
19:06:21.468
19:06:21.6
19:06:21.704
19:06:21.806
19:06:21.919
19:06:22.02
19:06:22.12
19:06:22.22
19:06:22.324
可以看到,在将近一秒后,确实是停止了,但是代码需要有其健壮性.尤其是中断线程这种重要的需求,不能只适用一种情况,不适用另一种情况
不适用的情况
private volatile boolean sign = true;
public static void main(String[] args) throws InterruptedException {
WrongWayVolatileSignStopInvalid stop = new WrongWayVolatileSignStopInvalid();
new Thread(() -> {
int num=0;
while (stop.sign) {
try {
Thread.sleep(100000);
num++;
if (num==5) System.out.println(4/0);
} catch (InterruptedException e) {
e.printStackTrace();
}catch (Exception e){
System.out.println(e);
}
System.out.println(LocalTime.now().format(DateTimeFormatter.ISO_TIME));
}
}).start();
Thread.sleep(1000);
stop.sign = false;
System.out.println(stop.sign);
}
false
可以看到,当上面的线程陷入阻塞的时候,volatile是无法停止线程的.
而在生产中,生产者消费者模式是一种常见的场景,如果不能满足这种场景的中断需求,明显是不合理的,有bug的
/**
* 陷入阻塞时,volatile无法使线程中断
* 例如生产者,消费者模式.因为消费者需要做很多数据处理,依赖下游服务,一般消费的速度都会比生产速度的慢
* ,当消费队列满了,一般就会让生产者陷入阻塞状态.这是很常见的一种业务模式
* 实际开发,实际生产线上服务的一种很常见情况
*/
public class WrongWayVolatileSignStopInvalid1 {
BlockingQueue<Integer> queue = null;
Thread producer;
int size;
volatile boolean canceled;
void init(int size) {
this.size = size;
this.queue = new ArrayBlockingQueue<>(size);
initProducer();
}
private void initProducer() {
this.producer = new Thread(() -> {
int num = 0;
try {
while (num <= 100000 && !canceled) {
if (num % 100 == 0) {
queue.put(num);
System.out.println("100的倍数>>>" + num + "已放入仓库");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("producer exit");
}
});
}
//通过随机数模拟不再需要数据的情况
public boolean needMoreNums() {
if (Math.random() > 0.95) return false;
return true;
}
public static void main(String[] args) throws InterruptedException {
WrongWayVolatileSignStopInvalid1 invalid = new WrongWayVolatileSignStopInvalid1();
invalid.init(10);
invalid.producer.start();
Thread.sleep(1000);
while (invalid.needMoreNums()){
System.out.println(invalid.queue.take()+"被消费");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据");
//一但消费者不需要更多数据,就让生产者停止
invalid.canceled=true;
System.out.println(invalid.canceled);
}
}
结果
100的倍数>>>0已放入仓库
100的倍数>>>100已放入仓库
100的倍数>>>200已放入仓库
100的倍数>>>300已放入仓库
100的倍数>>>400已放入仓库
100的倍数>>>500已放入仓库
100的倍数>>>600已放入仓库
100的倍数>>>700已放入仓库
100的倍数>>>800已放入仓库
100的倍数>>>900已放入仓库
0被消费
100的倍数>>>1000已放入仓库
100的倍数>>>1100已放入仓库
100被消费
消费者不需要更多数据
true
可以看到当希望中断生产者的时候,生产者并没有停止,而是挂起了.这明显是不符合预期的.且当根据新的参数创建新的生产者后,其依旧会挂起在哪里,又因为是GCRoot,永远不会回收,所以会一直挂起,浪费宝贵系统资源.
通过中断(interrupt)达到正确语义,修复等待问题
/**
* 通过中断(interrupt),修复等待问题
*/
public class WrongWayVolatileSignStopValid1 {
BlockingQueue<Integer> queue = null;
Thread producer;
int size;
void init(int size) {
this.size = size;
this.queue = new ArrayBlockingQueue<>(size);
initProducer();
}
private void initProducer() {
this.producer = new Thread(() -> {
int num = 0;
try {
while (num <= 100000 ) {
if (num % 100 == 0) {
queue.put(num);
System.out.println("100的倍数>>>" + num + "已放入仓库");
}
num++;
}
} catch (InterruptedException e) {
System.err.println(e);
} finally {
System.out.println("producer exit");
}
});
}
//通过随机数模拟不再需要数据的情况
public boolean needMoreNums() {
if (Math.random() > 0.95) return false;
return true;
}
public static void main(String[] args) throws InterruptedException {
WrongWayVolatileSignStopValid1 invalid = new WrongWayVolatileSignStopValid1();
invalid.init(10);
invalid.producer.start();
Thread.sleep(1000);
while (invalid.needMoreNums()){
System.out.println(invalid.queue.take()+"被消费");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据");
//一但消费者不需要更多数据,就让生产者停止
invalid.producer.interrupt();
System.out.println(invalid.producer.isInterrupted());
}
}
结果
100的倍数>>>0已放入仓库
100的倍数>>>100已放入仓库
100的倍数>>>200已放入仓库
100的倍数>>>300已放入仓库
100的倍数>>>400已放入仓库
...
1500被消费
100的倍数>>>2500已放入仓库
1600被消费
100的倍数>>>2600已放入仓库
消费者不需要更多数据
false
producer exit
java.lang.InterruptedException
Process finished with exit code 0
可以看到在这种方法下,程序按照语义正常的退出了,并没有因为是GC根和挂起,而无尽等待.
5.手写一个生产者消费者案例
/**
* 陷入阻塞时,volatile无法使线程中断
* 例如生产者,消费者模式.因为消费者需要做很多数据处理,依赖下游服务,一般消费的速度都会比生产速度的慢
* ,当消费队列满了,一般就会让生产者陷入阻塞状态.这是很常见的一种业务模式
* 实际开发,实际生产线上服务的一种很常见情况
*/
public class WrongWayVolatileSignStopInvalid {
LinkedList<String> queue = null;
Thread consumer, producer;
int size;
volatile int consumerNum;
void init(int size) {
this.size = size;
this.queue = new LinkedList<String>();
//作为消费者的锁使用
consumer = initConsumer(0);
initProducer();
}
private void initProducer() {
this.producer = new Thread(() -> {
try {
while (true) {
int index;
String temp;
synchronized (consumer) {
if (queue.size() != 0) consumer.notifyAll();
}
synchronized (queue) {
if (queue.size() == this.size) {
queue.wait();
}
temp = LocalTime.now()
.format(DateTimeFormatter.ISO_TIME);
queue.add(temp);
index = queue.size();
}
System.err.println("producer>>>" + index + ">>>" + temp);
//生产耗时
Thread.sleep(300);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
private Thread initConsumer(int num) {
return new Thread(() -> {
try {
while (true) {
String temp;
//consumer变量当前invalid实例唯一,确保所有消费者同时只能进入一个
synchronized (consumer) {
if (queue.size() == 0) {
//notify()确保有一个消费线程可以从等待中唤醒
//继续下面的逻辑,唤醒生产线程,可以考虑通过消费线程集合
//优化操作
//3代表当前消费线程总数减一,确保最后一个消费线程暂停前
//可以唤醒一个等待的消费线程,执行下面的唤醒生产线程的逻辑
if (consumerNum == 3) {
consumer.notify();
consumerNum = 0;
}
consumerNum++;
consumer.wait();
}
}
//queue变量当前invalid实例唯一,确保所有消费者同时只能进入一个
synchronized (queue) {
//每次都用queue.size()取值
//确保不被其它线程污染过的临时变量影响判断
if (queue.size() == 0) {
//唤醒生产线程
queue.notify();
//跳出当前循环,避免唤醒后queue.size依旧
//为0触发queue.getFirst()检查的异常的情况
continue;
}
temp = queue.pollFirst();
}
System.out.println(LocalTime.now()
.format(DateTimeFormatter.ISO_TIME) + "consumer" + num + ">>>" + temp);
//消费耗时
Thread.sleep(2500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
public static void main(String[] args) throws InterruptedException {
WrongWayVolatileSignStopInvalid invalid = new WrongWayVolatileSignStopInvalid();
invalid.init(5);
invalid.producer.start();
invalid.initConsumer(1).start();
invalid.initConsumer(2).start();
invalid.initConsumer(3).start();
invalid.initConsumer(4).start();
}
}
结果
producer>>>1>>>19:54:29.243
19:54:29.249consumer1>>>19:54:29.243
producer>>>1>>>19:54:29.549
producer>>>2>>>19:54:29.849
19:54:29.849consumer2>>>19:54:29.549
19:54:29.849consumer3>>>19:54:29.849
producer>>>1>>>19:54:30.149
producer>>>2>>>19:54:30.449
19:54:30.449consumer4>>>19:54:30.149
producer>>>2>>>19:54:30.75
producer>>>3>>>19:54:31.05
producer>>>4>>>19:54:31.351
producer>>>5>>>19:54:31.651
19:54:31.749consumer1>>>19:54:30.449
producer>>>5>>>19:54:31.952
19:54:32.35consumer3>>>19:54:30.75
19:54:32.35consumer2>>>19:54:31.05
19:54:32.95consumer4>>>19:54:31.351
19:54:34.249consumer1>>>19:54:31.651
19:54:34.85consumer3>>>19:54:31.952
producer>>>1>>>19:54:34.85
producer>>>2>>>19:54:35.151
19:54:35.151consumer2>>>19:54:34.85
producer>>>2>>>19:54:35.451
19:54:35.451consumer4>>>19:54:35.151
producer>>>2>>>19:54:35.751
producer>>>3>>>19:54:36.052
producer>>>4>>>19:54:36.353
Process finished with exit code -1
6.停止线程的相关重要函数
java.lang.Thread#interrupt
中断目标线程(Interrupts this thread),设置标记位,对特殊阻塞情况触发一个报错,对选择器则类似选择器的wakeup方法,直接返回等
java.lang.Thread#interrupted
/*Tests whether the current thread has been interrupted. The interrupted status of the thread is cleared by this method. In other words, if this method were to be called twice in succession, the second call would return false (unless the current thread were interrupted again, after the first call had cleared its interrupted status and before the second call had examined it).
A thread interruption ignored because a thread was not alive at the time of the interrupt will be reflected by this method returning false.
Returns:
true if the current thread has been interrupted; false otherwise.
测试当前线程是否已被中断。线程的中断状态由这个方法清除。换句话说,如果这个方法要连续调用两次,那么第二个调用将返回false(除非当前线程再次被中断,在第一个调用清除其中断状态之后,在第二个调用检查它之前)。
如果线程中断被忽略,因为在中断时线程不是活动的,则该方法将返回false。
返回:
如果当前线程已被中断,则为true;否则错误。*/
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
注意这个static的方法,目标线程是当前执行它的线程.
public static void main(String[] args) {
Thread first = new Thread(() -> {
for (; ; ) ;
});
first.start();
//预期结果
//false
System.out.println("first>>>" + first.isInterrupted());
//中断 false->true
first.interrupt();
//true->false
System.out.println("interrupted>>>" + first.interrupted());
//false
System.out.println("first>>>" + first.isInterrupted());
//实际结果,因为interrupted这个静态方法目标线程为调用线程,而不是调用它的
//实例对象线程
//所以结果为false-false-true
}
first>>>false
interrupted>>>false
first>>>true
通过源码的注解也可以看到
//Tests whether the current thread has been interrupted.
// currentThread()
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
java.lang.Thread#isInterrupted()
/*Tests whether this thread has been interrupted. The interrupted status of the thread is unaffected by this method.
A thread interruption ignored because a thread was not alive at the time of the interrupt will be reflected by this method returning false.
Returns:
true if this thread has been interrupted; false otherwise.
测试该线程是否已被中断。线程的中断状态不受此方法的影响。
如果因为在中断时线程已不再活跃,导致线程中断被忽略.则该方法将返回false。
返回:
如果线程已被中断,则返回:true;否则错误。*/
public boolean isInterrupted() {
return isInterrupted(false);
}
/*Tests if some Thread has been interrupted. The interrupted state is reset or not based on the value of ClearInterrupted that is passed.
测试某个线程是否被中断。中断状态是否重置取决于传递的ClearInterrupted的值。*/
private native boolean isInterrupted(boolean ClearInterrupted);