背景
这里将着重描述如何停止一个线程,同时,顺带对其他线程方法如join、yield、wait方法也做一个总结。
1. stop方法
这个方法其实很暴力。有点像站在命运掌控者的视角,让一个国家皇帝立刻马上去死。后事还没有安排,皇帝老爷就忽然驾崩了,导致后续皇权可能会平稳过度,也可能让国家陷入战乱。
我们来看一个生产消费者模型的实例:
public static void main(String args[]) {
Queue<Integer> buffer = new LinkedList<>();
int maxSize = 10;
Thread producer = new Producer(buffer, maxSize, "PRODUCER");
Thread consumer = new Consumer(buffer, maxSize, "CONSUMER");
new Thread(()->{
try {
Thread.sleep(300);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
producer.stop();
}).start();
producer.start();
consumer.start();
}
}
class Producer extends Thread {
private Queue<Integer> queue;
private int maxSize;
int counter =0 ;
public Producer(Queue<Integer> queue, int maxSize, String name) {
super(name);
this.queue = queue;
this.maxSize = maxSize;
}
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.size() == maxSize) {
try {
System.out.println("Queue is full.");
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
try {
System.out.println("Producing value : " + ++counter);
//通过睡眠,模拟生产过程
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
queue.add(counter);
queue.notifyAll();
}
}
}
}
class Consumer extends Thread {
private Queue<Integer> queue;
private int maxSize;
public Consumer(Queue<Integer> queue, int maxSize, String name) {
super(name);
this.queue = queue;
this.maxSize = maxSize;
}
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
System.out.println("Queue is empty.");
try {
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println("Consuming value : " + queue.remove());
queue.notifyAll();
}
}
}
说明:
- 生产过程需要假设需要500毫秒左右,然而我们在300毫秒左右的时候,关掉了生产者线程,这个时候,生产过程已经开始但是并没有完成;
- 当关闭生产者线程时,它会释放自己所持有的所有锁,此时消费者线程获取了queue锁,当进入消费过程,却发现队列是空的;
- 假设这样的场景:生产者线程是意外关掉的,而生产过程执行的是一件很重要的任务,数据很敏感,这时候通过stop这种暴力手段将可能带来一场灾难,所产生的后果是未知的。
JDK文档中说:
这个方法天生就是不安全的。通过这种方式停止一个线程,将导致该线程解锁所有的监视器对象(即上面中的queue)。如果某一对象(queue)的一致性受到该监视器的?;?,暴力停止线程所造成的对对象的破坏对其他线程是可见的。
其实,不仅仅是锁的问题,又比如读写文件的过程,在文件写入过程中,随便stop的线程,将导致写入内容无法被预测,即我们无法知道它写入了多少。
1.1 使用条件变量停止一个线程
一种比较安全的停止线程的方式是:告诉想要停止的线程,我想要关闭你,有些后事赶紧处理下。
比如,我们可以添加一个条件变量flag:
class TerminateClassByFlag extends Thread {
private volatile boolean terminateFlag;
private int counter;
public void terminate() {
terminateFlag = true;
}
@Override
public void run() {
while (!terminateFlag) {
try {
Thread.sleep(1000);
System.out.println("counter:" + counter++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在需要停止该线程的时候,调用terminate方法即可。但是,该方法存在一个问题,即在收到terminate指令之后,并不会立刻停止,比如它正在休眠当中。必须是在检查循环条件时,才会因不满足循环条件而退出。
1.2 使用线程的intercept方法停止线程
如果希望能够立刻退出线程,可以使用intercept方法。
public static void main(String[] args) {
Thread a = new Thread(()->{
int counter =0;
while(!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(1000);
System.out.println("cuonter:"+counter++);
} catch (Exception e) {
e.printStackTrace();
//注意这里,需要再次调用intercept
Thread.currentThread().interrupt();
}
}
});
a.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
a.interrupt();
}
intercept方法会中断该线程,中断过程又分了4种情况,JDK文档中说:
- 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)等线程方法,中断标志将被清除,同时会收到一个中断异常。(注意,这就是我在上面的代码中为什么要在catch中重新intercept)。- 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 ClosedByInterruptException.
如果该线程正阻塞于interruptible channel上的I/O操作,则该通道将被关闭,同时该线程的中断状态被设置,并收到一个java.nio.channels.ClosedByInterruptException。- If this thread is blocked in a 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.
如果该线程正阻塞于一个java.nio.channels.Selector操作,则该线程的中断状态被设置,它将立即从选择操作返回,并可能带有一个非零值,就好像调用java.nio.channels.Selector.wakeup()方法一样。- If none of the previous conditions hold then this thread's interrupt status will be set.
如果上述条件都不成立,则该线程的中断状态将被设置。
那么,该如何获取中断状态呢?就是通过isInterrupted()方法或者interrupted()方法。
其中前者属于对象的方法,后者是静态方法。并且后者在读取中断状态后,会清除中断状态。
2.join方法
join(), 对于线程A和线程B两个线程,如果在线程B中调用线程A的join方法,则B由wait方法进入阻塞状态,直到A执行完之后notifyAll重新唤醒B。
join实现源码为:
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
3. yield
JDK文档中指出:
该方法实际上是一个暗示,表明它希望放弃当前的处理器。这种暗示,可能被调度器忽略。
即它会重新进入到可执行状态,但是由于线程优先级缘故,调度器可能选择该线程继续执行。事实上,我没有使用过该方法。JDK文档中也说该方法非常少用 ,主要是帮助进行极少情况的bug重现。
4. wait
进入到监视器对象的阻塞队列中,直到调用了该对象的notify或者notifyAll方法,被唤醒从而拥有了获取监视器主权的条件。
其实,就是排队过独木桥,当一个对象过完独木桥,会调用notify方法,从队列中选取另外一个对象过桥,而notifyAll会唤醒所有等待过桥的对象,让它们去抢这个多桥的机会。
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}