java线程中断、睡眠、等待


零、前言

在应用线程的时候,启动后,线程就像脱缰的二哈一样不受控制的向前跑。那么怎么让它停下来呢(暂停或者完全结束),下面就带着这个问题探究一下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--");
	}

说明

  1. sleep是Thread类的其中一个静态方法
  2. sleep是个本地方法(Native)
  3. 抛出检查异常InterruptedException,所以需要捕获异常
  4. sleep方法需要传入睡眠时间,单位是毫秒。如果想用其他单位,可以用 TimeUnit.SECONDS.sleep(3)(jdk中concurrent包下的工具类)
  5. sleep用于让当前线程睡眠一段时间
  6. 线程睡眠的时候,当前线程不让出cpu(这是相对于后面的wait说的)
  7. 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,会产生两个效果:

  1. 释放掉自己获得得锁(一般进入同步块就代表着获得了锁,但如果连着使用两个wait,第一个wait被唤醒后,又执行第二个wait也是不报错的,此时就不存在释放锁的事了)
  2. 使synchronized所在的线程阻塞等待在wait这句话

唤醒(notify)

wait一般不单独使用,而是和notify配合使用,notify用于唤醒被wait的线程(但不会让它获得锁)。但需要注意的是,如果有多个线程被wait,那么唤醒哪一个并不一定,此时一般使用notifyAll()来唤醒所有wait线程。

wait(), object对象,线程三者关系

介绍完了wait使用的几个必要条件,那么再说说这些必要条件(wait,object对象,线程)之间的关系
在这里插入图片描述
上下两条蓝色的框代表两个线程,他们直接互不相干,由于两个同步块使用同一个对象锁,从而两个线程产生了联系。
每个线程里都有一段绿色的同步块。在同步块内使用wait,把当前的线程阻塞等待,并且释放掉锁。而notify则通过对象锁让等待的线程重新获得cpu,继续运行。
这里之所以画两个object对象,是为了表达:两个不同的对象锁的wait互不影响,同样,即使notifyAll也只是影响同一个对象锁的wait。

小结

  1. 使用wait的三要素:synchronized, wait, notify。wait/notify这对“作用相反”的方法,在synchronized的同步块中使用。
  2. 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就是一个线程对象】

在这里插入图片描述
方法解释:

  1. interrupt 中断线程(只是发送一个中断消息,修改线程中断标志,并没有真正的中断线程)
  2. interrupted 是Thread的一个静态方法,所以不要用对象调用。作用是测试当前线程的中断标志状态,如果线程收到中断标记,则为true,否则为false。并且将中断标记重置为false(未中断标记)
  3. 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也是被明确不建议使用的方法。

五、总结

  1. 短暂的停顿可以通过sleep的定时休眠
  2. 需要线程间通信完成启停的使用synchronized + wait/notify 或者 Lock + condition.await()/condition.signal()
  3. 需要立即停止的使用interrupt/interrupted,配合判断逻辑控制逻辑结束是比较安全可控的建议方法
  4. 程序中线程突然结束,一般就是内部异常没能捕获,导致抛出了异常。但不建议通过抛异常来作为逻辑一部分 让线程强制结束。同样也不建议使用thread.stop停止线程
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值