线程在其生命周期中,可能随时处于以下五种状态之一:
- 创建(new)状态: 当使用new创建一个新的线程对象时,该线程处于创建状态,此时系统还没有为它分配资源。
- 就绪(runnable)状态: 此时线程调用了start()方法,进入可执行状态,等待CPU进行调度
- 运行(running)状态: 线程获得CPU资源,执行run()方法
- 阻塞(blocked)状态: 因sleep()或wait()或其他线程join()或等待IO等原因,暂时停止执行,让出CPU资源进入阻塞状态
- 终止(dead)状态: 线程执行完毕,或被杀死,此时线程销毁
有意思的是阻塞有三种阻塞:
1.持有锁资源式阻塞:这种阻塞方式不会释放当前线程已持有的锁资源
2.等待池:进入等待池的线程会将持有的锁资源释放
3.锁池:进入锁池的线程,只需获取到执行所需的锁资源就可以进入可执行状态了,有种“万事俱备只欠东风”的感觉,但它仍处于等待状态(Block)中
实现Runnable接口或继承Thread可以得到一个线程类,new一个该类的实例出来,线程就进入了创建(new)状态。调用线程的start()方法,此线程进入就绪(runnable)状态。就绪状态只是说该线程处于随时可运行的状态,但具体是否运行,何时运行由cpu决定,cpu没有”临幸”就只能等着。当时间片用完、sleep()结束、join()加入的其他线程执行完毕(并成功获取到锁)、IO输入完成、线程拿到别的线程释放的自己所需的同步锁等情况出现时,线程进入就绪状态。需要注意的是,yield()方法是线程主动建议cpu结束自己的时间片,让给其他线程执行,但是否采纳建议,由CPU决定
线程调度程序从可运行池(Runnable)中选择一个线程,为其分配cpu资源,进入运行(running)状态。这也是线程进入运行状态的唯一一种方式。
因某种原因,线程进入不可继续执行状态,这种状态被称为阻塞(blocked)状态
以下情况会出现线程阻塞状态:
1.线程调用sleep()方法,主动放弃占用的处理器资源,但不会释放自己所持有的锁资源,等待sleep()时间到重新进入可执行状态
2.线程等待IO输入,等待IO输入完成重新进入可执行状态,IO阻塞是不会释放锁的
3.线程试图获得一个同步锁(synchronized),但该同步锁正被其他线程所持有,等待获取锁后重新进入可执行状态。
4.线程被wait()方法阻塞同时会释放自己持有的同步锁,然后等待notify()方法唤醒或wait()时间到,此时进入同步锁阻塞状态(第3种阻塞情况),获取到锁之后才能进入可执行状态
5.线程被其他线程通过join()方法打断执行,待通过join()加入的线程执行完毕后再重新进入可执行状态,由于join()底层是wait()方法,因此join()会释放锁
当线程的run()方法完成时,或者线程抛出一个未捕获的Exception或Error,或者通过stop()方法结束该线程(stop极度危险),我们就认为它进入终止(dead)状态。这个线程对象也许是活的。但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。在一个终止的线程上再次调用start()方法,会抛java.lang.IllegalThreadStateException异常。
下面我们来盘点一下多线程中常用的方法
1.yeild()
线程调用yield()方法意味着线程告诉JVM自己正在执行的任务并不紧急,”暗示”虚拟机自己可以让出CPU资源给其他线程使用,但具体是否会出让资源并不保证,由CPU决定是否切换线程。当然了,即使出让CPU,线程也是转入可执行状态,并不会释放自己所持有的锁资源,随时准备再次执行2.wait()和notify()/notifyAll()
这三个方法都是java.lang.Object的方法
2.1 锁池和等待池
在讨论这三种方法之前,首先讨论两个概念,这两个概念实际上我们之前也有提过:锁池:假设线程A已经拥有了某个锁资源,而其它的线程想要调用这个锁资源对象的某个synchronized方法(或者synchronized块),由于这些线程在进入synchronized方法(或者synchronized块)之前必须先获得该锁的拥有权,但是该锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中,当锁资源被线程A释放后,锁池中的线程会竞争 锁资源,获取到锁的线程将进入可执行状态。参见上文导致阻塞的第3种情况
等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中,等待池中的线程在被唤醒前不会竞争锁资源。参见上文导致阻塞的第4种情况
2.2 wait()和notify()/notifyAll()
对某个 锁资源对象 调用wait()方法会使当前线程进入该锁对象的等待池中,并释放线程自己所持有的该 锁资源对象 的锁,直到其他线程调用此 锁资源对象 的notify()方法或 notifyAll()方法唤醒它。wait()方法当然也可以设置一个long类型的等待时间,一旦超时也会自动唤醒
对 锁资源对象 调用notify()方法,将从该 锁资源对象 的等待池中 随机 选择一个线程进入该 锁资源对象 的锁池中,和锁池中的其他资源竞争锁
对 锁资源对象 调用notifyAll ()方法,会将该 锁资源对象 的等待池中所有的线程都唤醒到锁池中一起来竞争锁资源
需要注意的是,由于wait()方法会释放自己持有的锁资源,因此,该方法必须放在synchronized同步代码块中使用,否则抛出IllegalMonitorStateException异常,notify/notifyAll方法也是如此
另外,调用notify/notifyAll方法后,被唤醒的线程也不会立即进入可执行状态,只是被从锁对象的等待池中移入了锁对象的锁池。接下来该线程会尝试和锁池中的其他线程竞争锁资源,如果获取锁成功,才能进入可执行状态
下面是一个简单的示例:
class Waiter implements Runnable{
private String msg;
public Waiter(String msg) {
super();
this.msg = msg;
}
@Override
public void run() {
synchronized (msg) {
System.out.println(Thread.currentThread().getName()+"获取msg控制权");
try {
Thread.sleep(10);
msg.wait();
System.out.println(Thread.currentThread().getName()+"被恢复");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Notifyer implements Runnable {
private String msg;
public Notifyer(String msg) {
super();
this.msg = msg;
}
@Override
public void run() {
synchronized (msg) {
System.out.println("notify开始工作");
try {
Thread.sleep(100);
msg.notify();
System.out.println("notify工作完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Test
public void test_2() {
String msg=new String("------");
new Thread(new Waiter(msg),"waiter").start();
new Thread(new Notifyer(msg),"notify").start();
while (true){}
}
2.3 由notify()引发的死锁
notify()在使用不当时可能导致死锁 ,如下例://生产者
class Producer implements Runnable {
private String threadName;
//缓冲队列,存储生产者产出的Integer
List<Integer> cache;
public Producer(List<Integer> cache,String threadName) {
this.cache = cache;
this.threadName=threadName;
}
@Override
public void run() {
//生产者线程一旦被唤醒就会不断生产
while (true) {
produce();
}
}
private void produce() {
synchronized (cache) {
try {
System.out.println("**********生产者"+this.threadName+"获取锁**********" );
//首先判断缓冲队列中是否已存在数据,如果存在就把自己移入缓冲队列的等待池中
while (cache.size() > 0) {
System.out.println("缓存不为空,生产者"+this.threadName+"进入等待池" );
cache.wait();
}
// 跳出循环,意味着cahce为空,接下来一秒生产一条消息
Thread.sleep(1000);
System.out.println("生产者"+this.threadName+"生产数据!");
cache.add(new Random().nextInt());
//从等待池中 随机 唤醒一个线程
cache.notify();
System.out.println("**********生产者 "+this.threadName+"从等待池中随机唤醒一个线程并释放锁**********");
}
catch (Exception e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private String threadName;
//缓冲队列,消费者从中获取Integer消费
List<Integer> cache;
public Consumer(List<Integer> cache,String threadName) {
this.cache = cache;
this.threadName=threadName;
}
@Override
public void run() {
//消费者线程一旦被唤醒就会不断消费
while (true) {
consume();
}
}
private void consume() {
synchronized (cache) {
try {
System.out.println("**********消费者"+this.threadName+"获取锁**********" );
//首先判断缓冲队列中是否为空,如果没有产品就把自己移入缓冲队列的等待池中
while (cache.isEmpty()) {
System.out.println("缓存为空,消费者"+this.threadName+"进入等待池" );
cache.wait();
}
//消费产品
System.out.println("消费者 "+this.threadName+"消费了数据"+cache.remove(0) + "!");
cache.notify();
System.out.println("**********消费者 "+this.threadName+"从等待池中随机唤醒一个线程并释放锁**********");
}
catch (Exception e) {
e.printStackTrace();
}
}
}
}
如上例的消费者和生产者,如果我们只启动一个消费者和一个生产者两条线程,那么一切正常,等待池中始终只有一个线程,要么就是生产者唤醒消费者,要么消费者唤醒生产者,不会发生死锁:
@Test
public void test() {
//一个消费者一个生产者
List<Integer> cache=new ArrayList<>();
new Thread(new Consumer(cache,"C")).start();
new Thread(new Producer(cache,"P")).start();
while (true){}
}
如果开启了一个生产者和两个消费者,且使用notify()的方式来唤醒线程,那么则可能发生死锁:
@Test
public void test_2() {
//两个消费者一个生产者
List<Integer> cache=new ArrayList<>();
new Thread(new Consumer(cache,"C1")).start();
new Thread(new Consumer(cache,"C2")).start();
new Thread(new Producer(cache,"P")).start();
while (true){}
}
输出结果如下:
解决方法是将notify换成notifyAll
3.interrupt()
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。interrupt翻译过来是”终断,打断”的意思,它被用于终止线程,但 interrupt()并不直接中断线程,而是通过给线程设置一个中断标识(每个线程中都有一个与中断相关联的 boolean 属性,用来表示线程的中断状态),传达”线程应该被中断了”的信息,然后由我们自己编写的代码进行中断检查,再决定是否中断,他只是设置了一个特殊的标识位,并未起到任何实际作用。(实际上,interrupt()不能不能真正意义上地中断正在运行过程中的线程,但能通过抛出中断异常的方式真正中断处于阻塞状态中的线程)
那么哪些场景中需要用到中断机制呢?
1.很多线程的运行模式是死循环,比如在生产者/消费者模式中,消费者主体就是一个死循环,它不停的从队列中接受任务,执行任务,在停止消费者程序时,我们需要一种”优雅”的方法以关闭该线程。
2.抢12306的火车票,我们可能开启多个线程购买火车票,只要有一个线程买到了,我们就需要中断其他线程。
3.从第三方服务器查询一个结果,我们希望在限定的时间内得到结果,如果得不到,我们会希望取消该任务。此时需要中断查询的线程
……
在了解中断机制前,我们先来看看与中断标识相关的三个方法:
- public boolean isInterrupted() //返回线程当前的中断状态
- public void interrupt() //设置线程的中断状态(设为true)
- public static boolean interrupted()//返回线程当前的中断状态,然后清除中断状态(设为false)
对于处于不同状态的线程,调用 interrupt()的效果是不一样的:
1. 对处于运行(running)状态的线程使用 interrupt()
interrupt()只是会设置线程的中断标志位,没有任何其它作用,不会打断线程的执行,为了能够正常终止线程,编写线程时应该在合适的位置检查中断标志位,比如:
//生产者
class Producer implements Runnable {
private String threadName;
//缓冲队列,存储生产者产出的Integer
List<Integer> cache;
public Producer(List<Integer> cache,String threadName) {
this.cache = cache;
this.threadName=threadName;
}
@Override
public void run() {
long beginTime=System.currentTimeMillis();
//生产者线程会不断生产,直至被打断
while (!Thread.currentThread().isInterrupted()) {
//5s后打断生产者
if(System.currentTimeMillis()-beginTime>500){
Thread.currentThread().interrupt();
System.out.println("打断线程"+Thread.currentThread().isInterrupted());
}
produce();
}
}
private void produce() {
synchronized (cache) {
try {
cache.add(new Random().nextInt());
System.out.println("生产者生产了数据" );
}
catch (Exception e) {
e.printStackTrace();
}
}
}
}
@Test
public void test() {
List<Integer> cache=new ArrayList<>();
Thread thread=new Thread(new Producer(cache,"P"));
thread.start();
while (true){}
}
2. 对处于阻塞(block)状态的线程使用 interrupt()
对于调用了sleep(), wait(), join()等方法而进入阻塞状态的线程,若此时调用线程的interrupt(),会先将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除(重置为false),同时抛出一个InterruptedException异常,从而退出阻塞(block)状态(抛出异常会导致被阻塞的线程重新开始执行,不过此时执行的是catch块中的代码)
@Override
public void run() {
try {
while (true) {
// 执行任务
}
} catch (InterruptedException e) {
// 由于产生InterruptedException异常,退出while(true)循环,线程终止!
}
}
Thread.stop, Thread.suspend, Thread.resume等中断线程的方法极度危险,现行版本的jdk已将其标为废弃方法,应尽量避免使用
4. sleep()
Thread.sleep(long millis)和Thread.sleep(long millis, int nanos)静态方法强制当前正在执行的线程休眠(暂停执行),以“减慢”线程执行速度。
和wait()方法不同,线程睡眠并不会释放锁资源。因此当线程睡眠时间到期后,线程不需要进入锁池获取锁,而是直接进入可执行状态等待cpu调度
需要注意的是sleep()中指定的时间是线程暂停运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就立即开始执行,只能保证睡眠到期后进入可执行状态。
5. join()
在某些情况下,主线程创建并启动了子线程,如果子线程中需要进行大量的耗时运算,主线程往往将早于子线程结束之前结束,如果主线程想等待子线程执行完毕后,获得子线程中的处理完的某个数据,就要用到join方法了,它等待调用join方法的子线程结束,再继续执行主线程,这使得主线程和子线程的并行执行变为串行执行
join底层是wait方法,所以子线程调用该方法时,暂停主线程的同时也会释放主线程持有的锁资源,而sleep在同步的方法中是不释放锁资源的,只有同步方法执行完毕,其他线程才可以执行。
6. currentThread ()
Thread类提供了一个静态方法currentThread ()用以获得当前正在执行的线程,有点类似this
7. isAlive()
线程处于创建(new)状态时,调用isAlive()方法返回false。在线程的run()方法结束之前,即没有进入死亡状态之前,线程调用isAlive()方法返回true.
本文原创https://blog.csdn.net/RicardoDing/article/details/82745910,转载请与本人联系: