线程状态及Thread常用方法详解
I. 线程状态
先来一个图,说明下线程的五个状态
1. 创建
顾名思义,就是创建了一个线程,也就通过 new Thread()
触发
2. 就绪状态
就绪,表示线程已经准备好了,随时可以进入运行,
当 start()
调用之后,线程进入就绪状态,这个时候是准备运行,但是并没有执行
3. 运行状态
表示线程在执行了,真正工作跑任务
4. 阻塞状态
线程运行之后,发生了一些变故,需要挂起时,这时就进入阻塞,把cpu和资源让给其他的线程去执行;这个就是阻塞状态了
也就是说,必须是有运行状态进入阻塞状态
5. 结束
线程执行完了,也是时候收拾收拾,各回各家了,就表示这个线程该干的活干完了,到过河拆桥的时候了,赶紧把这个线程丢到垃圾堆吧(线程回收),这个状态就是线程结束(或者说线程死亡状态)
上面说了五个线程状态,各是什么意思,下面简单说下他们的关系
以一个线程的使用流程为例
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 创建一个线程
Thread thread = new Thread(() -> {
try {
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动线程
thread.start();
// 主线程挂起,保证thread线程逻辑进入并执行
Thread.sleep(2000);
// 主线程向队列中塞一个数据,唤醒thread线程
queue.put("hello world");
// 等待线程执行完毕
thread.join();
// 线程执行结束
System.out.println("---over---");
创建线程有四种方式,可以参考 ,
结合上面的case,分析下五种状态的转换过程:
- 首先是通过new来创建一个线程对象
thread
, 这儿时候,线程就处于创建状态了 - 接着我需要线程工作了,然后调用
thread.start()
方法,来启动线程
- 这个时候,线程并不会直接运行,此时会进入就绪状态(进入可运行线程池),也就表示我准备好了,随时可以工作
- 那么什么时候工作呢?这个就不由我们来控制了,实际是由线程调度程序从可运行线程池中挑一个线程来工作
- 运气来了,thread线程执行了,假设其从一个阻塞队列queue中取数据
- 然而此时queue为空,导致获取不到数据,线程被阻塞,等待队列非空,这个时候线程就由运行状态进入阻塞状态了
- 主线程此时往队列中塞入一个数据,thread线程被唤醒,此时依然是进入就绪状态,等待线程调度程序来执行它
- 等线程执行完毕后,就进入了死亡状态,然后就开始gc回收资源了
II. Thread解析
这里也不讲Thread是怎么工作的,实现原理啥的,比较复杂,我也莫不准,就从使用角度出发,来看看里面常用的方法,都是干嘛用的,以及什么时候用
1. start 方法
第一个就是这个start()方法了,启动线程
执行该方法之后,线程进入就绪状态,对使用者而言,希望线程执行就是调用的这个方法(注意调用之后不会立即执行)
这个方法的主要目的就是告诉系统,我们的线程准备好了,cpu有空了赶紧来执行我们的线程
2. run 方法
这个就有意思了,我们采用继承Thread类来创建线程时,需要覆盖的就是这个方法,把线程执行的业务逻辑,放在这个方法里面,但是线程的执行,却是start()
方法
run 方法中为具体的线程执行的代码逻辑,一般而言,都不应该被直接进行调用
那么问题来了,如果直接调用了会怎样?
直接调用Thread的run方法,并不会报错,且可以正常执行,但是执行是在调用这个方法的线程中执行的,不会让thread这个线程进入就绪状态,运行状态啥的,其实质就是一个普通对象的普通方法调用
3. sleep 方法
我们最常见的一种使用方式是在主线程中直接调用 Thread.sleep(100) , 表示先等个100ms, 然后再继续执行
4. wait 方法
wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程
通常我们执行wait方法是因为当前线程的执行,可能依赖到其他线程,如登录线程中,若发现用户没有注册,则等待,等用户注册成功后继续走登录流程(我们不考虑这个逻辑是否符合实际),
这里就可以在登录线程中调用 wait方法, 在注册线程中,在执行完毕之后,调用notify方法通知登录线程,注册完毕,然后继续进行登录后续action
5. yield 方法
这个方法的执行,有点像一个拿到面包的人对另外几个人说,我把面包放在桌上,我们从新开始抢,那么下一个拿到面包的还是这些人中的某个(大家机会均等)
想象不出啥时候会这么干
6. join 方法
从上面的描述也可以很容易看出什么场景需要调用这个方法,主线程和子线程谁先结束不好说,如果主线程提前结束了,导致整个应用都关了,这个时候子线程没执行完,就呵呵了;
其次就是子线程执行一系列计算,主线程会用到计算结果,那么就可以执行这个方法,保证子线程执行完毕后再使用计算结果
7. setDaemon 方法
这个比较有意思,将线程定义为守护线程,那么什么是守护线程?
这里有几点需要注意:
- thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程
- 在Daemon线程中产生的新线程也是Daemon的。
- 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑
因为你不可能知道在所有的User完成之前,Daemon是否已经完成了预期的服务任务。一旦User退出了,可能大量数据还没有来得及读入或写出,计算任务也可能多次运行结果不一样。这对程序是毁灭性的。造成这个结果理由已经说过了:一旦所有User Thread离开了,虚拟机也就退出运行了
III. 小结
1. 线程状态
线程有五个状态
- new一个线程对象后,首先进入创建状态
- 执行Thread#start方法之后,进入就绪状态
- 线程调度程序将就绪状态的线程标记为运行状态并真正运行
- 线程运行过程中,可以挂起,进入阻塞状态,阻塞状态恢复后,接着进入就绪而不是立马又恢复运行状态
- 线程执行完了,就进入结束/死亡状态
2. Thread使用注意
- 线程执行的业务逻辑,放在
run()
方法中 - 使用
thread.start()
启动线程 - wait方法需要和notify方法配套使用
- 守护线程必须在线程启动之前设置
- 如果需要等待线程执行完毕,可以调用
join()
方法