目录
零、前言
在应用线程的时候,启动后,线程就像脱缰的二哈一样不受控制的向前跑。那么怎么让它停下来呢(暂停或者完全结束),下面就带着这个问题探究一下java给我们提供了哪些可用的方法。
一、线程状态图
通过这张图可以看出:
能让线程从running——>阻塞、等待、死亡的有sleep、wait,synchronized,异常退出。(join是让另一个线程“插个队优先走”,yield是让当前线程让出一下cpu,然后重新抢cpu。都是不太常用并且也算不上停止线程,这里就不讨论这两个方法)
二、睡眠(sleep)
sleep是比较常见的一个方法,一般用于测试代码中,模拟某个执行场景。
Demo
public void m() {
System.out.println("--start--");
//当前线程等待3秒
try {
Thread.sleep(3000);//3000毫秒=3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("--end--");
}
说明
- sleep是Thread类的其中一个静态方法
- sleep是个本地方法(Native)
- 抛出检查异常InterruptedException,所以需要捕获异常
- sleep方法需要传入睡眠时间,单位是毫秒。如果想用其他单位,可以用
TimeUnit.SECONDS.sleep(3)
(jdk中concurrent
包下的工具类) - sleep用于让当前线程睡眠一段时间
- 线程睡眠的时候,当前线程不让出cpu(这是相对于后面的wait说的)
- sleep的唤醒只能通过定时,无法由其他方法唤醒(除了后面会说到的中断)
二、等待(wait)
wait是Object对象的其中一个方法,一般和synchronized配合使用,用于线程间的通讯调度方案。用于阻塞当前线程,并且释放已经获取的锁。
Demo
下面的例子,用两个线程分别代表两个宠物,模拟两个宠物争抢一个碗里的食物的场景。
import java.util.ArrayList;
import java.util.List;
public class TestWait {
//一次只能一个宠物吃饭,需要加锁,
Object o = new Object();
//一碗肉
List<String> bowl = new ArrayList<>();
public void timeToEat() {
Thread cat = new Thread(new GetFood("猫", "鱼"), "猫");
Thread dog = new Thread(new GetFood("狗", "骨"), "狗");
cat.start();//猫开始抢吃的
dog.start();//狗开始抢吃的
}
//猫喜欢吃鱼,狗喜欢吃骨头。他们只吃自己喜欢吃的
public boolean eat(String who, String meatType) {
String meat = bowl.get(0);
if (meat.indexOf(meatType) != -1) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(who + "吃掉" + meat);
//吃掉
bowl.remove(0);
return true;
} else {
return false;//不喜欢吃
}
}
class GetFood implements Runnable {
private String pet;//宠物
private String likeFood;//喜欢的食物
public GetFood(String pet, String likeFood) {
this.pet = pet;
this.likeFood = likeFood;
}
@Override
public void run() {
while(true) {//碗里还有肉就一直吃
synchronized (o) {
if (bowl.size() == 0) {
//碗里没肉了,通知一下其他宠物(否则其他人可能还在一边傻等)
o.notify();
System.out.println(pet + ":碗里没肉了,用餐结束");
break;
}
if (!eat(pet, likeFood)) {
try {
//发现不是自己喜欢吃的,就把碗让出来。自己先等待
o.notify();//把其他等待的线程唤醒
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
/**
* 主人在一个碗里装了各种食物,有些是猫喜欢吃的有些是狗喜欢吃的
* 吃饭时间到了...
* @param args
*/
public static void main(String[] args) {
TestWait tw = new TestWait();
tw.bowl.add("鲤鱼");
tw.bowl.add("草鱼");
tw.bowl.add("牛骨");
tw.bowl.add("黄花鱼");
tw.bowl.add("猪骨");
tw.bowl.add("带鱼");
tw.timeToEat();
}
}
执行结果
猫吃掉鲤鱼
猫吃掉草鱼
狗吃掉牛骨
猫吃掉黄花鱼
狗吃掉猪骨
猫吃掉带鱼
猫:碗里没肉了,用餐结束
狗:碗里没肉了,用餐结束
再使用jvisualvm直观的看一下两个线程的执行情况
从线程的执行上可以看出,猫和狗轮流等待(黄色),一个等待的时候另一个休眠(紫色)(这里是用sleep模拟宠物花5秒进食)
说明
这个例子的逻辑:
抢到锁(饭)的先执行(吃),如果发现上面不是自己喜欢吃的(就当宠物比较懂事,不会往下翻,只会一层一层的吃),就先通知(唤醒)一下另外的线程,然后自己等待(wait)。如果是自己喜欢吃的就吃掉,然后也通知一下其他人,大家再一次开始抢。这么循环,直到发现list(碗里)没有东西了,相互通知一下结束线程。
wait使用方式
在上面这个例子中,我们用了wait,还用了notify,还有锁(synchronized), 对于没用过的同学可能有点懵:为什么要搞这么复杂,不能像sleep那样,直接用wait,让当前线程处于等待吗
public void m() {
Object o = new Object();
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
是的,不能,这样执行是会报错的java.lang.IllegalMonitorStateException
下面接着说为什么
synchronized
wait的使用环境:在synchronized的同步块中,所以直接使用wait是会报错的。这里为什么强调是synchronized,而不是其他锁。因为wait就是只能配合synchronized使用,Lock是不能配合wait使用的。当在同步块中使用wait,会产生两个效果:
- 释放掉自己获得得锁(一般进入同步块就代表着获得了锁,但如果连着使用两个wait,第一个wait被唤醒后,又执行第二个wait也是不报错的,此时就不存在释放锁的事了)
- 使synchronized所在的线程阻塞等待在wait这句话
唤醒(notify)
wait一般不单独使用,而是和notify配合使用,notify用于唤醒被wait的线程(但不会让它获得锁)。但需要注意的是,如果有多个线程被wait,那么唤醒哪一个并不一定,此时一般使用notifyAll()来唤醒所有wait线程。
wait(), object对象,线程三者关系
介绍完了wait使用的几个必要条件,那么再说说这些必要条件(wait,object对象,线程)之间的关系
上下两条蓝色的框代表两个线程,他们直接互不相干,由于两个同步块使用同一个对象锁,从而两个线程产生了联系。
每个线程里都有一段绿色的同步块。在同步块内使用wait,把当前的线程阻塞等待,并且释放掉锁。而notify则通过对象锁让等待的线程重新获得cpu,继续运行。
这里之所以画两个object对象,是为了表达:两个不同的对象锁的wait互不影响,同样,即使notifyAll也只是影响同一个对象锁的wait。
小结
- 使用wait的三要素:synchronized, wait, notify。wait/notify这对“作用相反”的方法,在synchronized的同步块中使用。
- wait的作用,一个是让自己的线程等待,二是释放锁。但自己释放锁仅仅意味着别人可以获得锁了,如果另一个线程处于等待状态,那么它并不会因为这个线程等待了,它就被自动唤醒。
使用Lock实现类似的功能
前面说了wait/notify配合synchronized可以实现线程间的信息调度机制。那么Lock锁是否也可以呢,是的,Lock也可以实现类似的功能。需要配合condition.await()/condition.signal()。 用法基本一致,也是必须在同步块内使用,以实现线程等待,释放锁的效果。示例代码:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* synchronized 可以配合 object.wait()/ object.notify()实现线程之间的交互
* lock也有一套类似的实现机制
* 参考https://www.cnblogs.com/cyl048/p/10882013.html
* @author yuancan
*
*/
public class TestWait4 {
volatile boolean isProcess = false;
public void m() {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
System.out.println("-------T1------");
lock.lock();
isProcess = true;
//释放锁
try {
condition.await();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println("--T1---over");
},"T1").start();
// System.out.println("-------T2----a--");
// condition.signalAll();//在同步块外使用报错java.lang.IllegalMonitorStateException
// System.out.println("--T2--a-over");
new Thread(() -> {
lock.lock();
System.out.println("-------T2------");
condition.signal();
System.out.println("--T2---over");
lock.unlock();
},"T2").start();
}
public static void main(String[] args) {
TestWait4 tw = new TestWait4();
tw.m();
}
}
三、中断(interrupt)
前面说的sleep和wait都是暂时阻塞线程,那么如果想直接中断线程呢,就需要用到interrupt(中断)。
一般用法
Demo
public class InterruptAndInterrupted {
public void m() {
Thread t = new Thread(() -> {
for (int i = 0; i < 200; i++) {
System.out.println(i);
//测试是否有中断消息,并且清除中断标记。(注意这是个静态方法,作用于当前所属线程,不要用对象调用)
if (!Thread.interrupted()) {
System.out.println(i);
} else {
System.out.println("收到线程中断消息,结束线程");
break;
}
}
}, "T1");
t.start();
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();//中断(只是“标记”线程为中断,是一种线程通信机制,线程是否真的停止,只能由线程自己决定)
//false。因为线程里已经执行了Thread.interrupted(),中断标记已经被清除,所以是false
System.out.println("t.isInterrupted() : " + t.isInterrupted());
}
public static void main(String[] args) {
InterruptAndInterrupted ta = new InterruptAndInterrupted();
ta.m();
}
}
说明
代码逻辑:起一个线程T1,等2毫秒,把T1中断,输出一下T1是否已经被中断。
这个例子中涉及到了:Thread.interrupted(),t.interrupt(), t.isInterrupted() 【这里的t就是一个线程对象】
方法解释:
- interrupt 中断线程(只是发送一个中断消息,修改线程中断标志,并没有真正的中断线程)
- interrupted 是Thread的一个静态方法,所以不要用对象调用。作用是测试当前线程的中断标志状态,如果线程收到中断标记,则为true,否则为false。并且将中断标记重置为false(未中断标记)
- isInterrupted 测试中断标志状态(和interrupted的区别就是少了一步重置中断标志)
中断sleep
在用sleep方法时候,需要捕获一下异常InterruptedException,那么这个异常是如何出发的呢,触发之后什么呢,会不会停止。
下面就带着问题看看中断interrupt的其他“功能”(之所以加引号,是因为我们一般可能并不会用这样的特性去设计代码逻辑)
Demo
public class InterrupteSleep {
public void m1() {
Thread T1 = new Thread(() -> {
System.out.println("------------1");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("------------2");
});
T1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-----------interrupt--");
T1.interrupt();
System.out.println("-----------over");
}
public static void main(String[] args) {
InterrupteSleep ts = new InterrupteSleep();
ts.m1();
}
}
代码逻辑:起一个线程T1, 该线程正常要先打印一个1,然后睡眠10秒,最后打印2。 但主线程在一秒时执行了一句中断语句。
执行结果:
------------1
-----------interrupt--
-----------over
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.yc.thread.interrupte.InterrupteSleep.lambda$0(InterrupteSleep.java:9)
at java.lang.Thread.run(Thread.java:745)
------------2
说明
执行线程中断语句,可以使得该线程内的sleep被中断并且抛出异常InterruptedException,不再继续睡眠,但不影响后面的程序执行。
中断wait
和sleep类似,interrupt同样也能中断wait。
Demo
public class InterruptWait {
public void m() {
Object o = new Object();
Thread t1 = new Thread(() -> {
synchronized(o) {
System.out.println("--------------1-");
try {
o.wait();
//这句话没输出:因为在捕获到o.wait()异常之后就跳到catch中了。
//所以告诉我们一个道理:try{}的范围大小决定了程序的逻辑走向,如果想捕获异常后 下面的逻辑都不走了那么
//括号范围就大些,如果想捕获异常后依然把剩下的走完(一般就是前后没有因果关系的那种)就把try{}缩小点
System.out.println("after wait: " + Thread.currentThread().isInterrupted());
} catch (InterruptedException e) {
System.out.println("after wait2: " + Thread.currentThread().isInterrupted());//false
e.printStackTrace();
}
System.out.println("--------------2-");
}
});
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
try {
Thread.sleep(1);//这里如果不暂停一下,下面会检测到:刚被打断,但中断标记还没被清除的很短暂的一个中间状态
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t1.isInterrupted());//false。wait被中断后,中断标记被清除
}
public static void main(String[] args) {
InterruptWait tw = new InterruptWait();
tw.m();
}
}
执行结果:
--------------1-
after wait2: false
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.yc.thread.interrupte.InterruptWait.lambda$0(InterruptWait.java:16)
at java.lang.Thread.run(Thread.java:745)
--------------2-
false
说明:
interrupt中断wait后,线程不再阻塞,但并没有因此重新获得锁。
四、停止
前面也说了,interrupt并没有真正的中断线程,只是改变了线程的“中断标记状态”。只是我们可以配合interrupte控制程序在收到中断消息后的逻辑走向。那么有什么办法可以让线程立即强制终止呢。有两种方法: 1. 抛异常 2. 使用thread.stop()方法
Demo
/**
* 强制停止线程
* 两个思路:1. 用stop 2. 抛异常
* 都不建议使用
*/
public class StopThread {
/**
* 使用Thread的stop方法强制停止
*/
public void testStop() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.stop();
}
/**
* 使用抛异常的方式停止线程
*/
public void testExceptionToStop() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i > 2) {
int c = 2/0;
}
}
});
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
StopThread st = new StopThread();
// st.testStop();
st.testExceptionToStop();
}
}
小结
抛异常停止线程,一般是线程内没有正常捕获异常,导致的程序错误,一般不会用于程序逻辑。而thread.stop也是被明确不建议使用的方法。
五、总结
- 短暂的停顿可以通过sleep的定时休眠
- 需要线程间通信完成启停的使用synchronized + wait/notify 或者 Lock + condition.await()/condition.signal()
- 需要立即停止的使用interrupt/interrupted,配合判断逻辑控制逻辑结束是比较安全可控的建议方法
- 程序中线程突然结束,一般就是内部异常没能捕获,导致抛出了异常。但不建议通过抛异常来作为逻辑一部分 让线程强制结束。同样也不建议使用thread.stop停止线程