一、状态是线程的状态
状态是PCB中与调度相关的属性,线程是CPU调度执行的基本单位。所以,状态是线程的属性。谈到状态,考虑的都是线程的状态,不是进程!!!
二、在Java中,线程的状态有哪几种?
在Java中,线程的状态比操作系统内核中的更加详细, 线程的状态有以下六种:
状态 | 说明 |
NEW | 创建了Thread对象,但没调用start(内核中还没有创建对应的PCB) |
TERMINATED | 内核中的PCB已经执行完销毁了,Thread对象还在 |
RUNNABLE | 可运行的(正在CPU上执行的 和 在就绪队列中随时可以去CPU上执行 这两种统一都叫做RUNNABLE状态,不做区分,也区分不了) |
WAITING | 阻塞(线程PCB正在阻塞队列中) |
TIMED_WAITING | 阻塞(线程PCB正在阻塞队列中) |
BLOCKED | 阻塞(线程PCB正在阻塞队列中) |
对这六种状态,下面进行详细介绍:
1、首先, 主线任务是,NEW,RUNNABLE,TERMINATED
比如下面的代码(当run方法中没有sleep或其他会造成线程阻塞的代码时):
刚开始,创建了Thread对象,但还没执行到start方法,这时,线程的状态就是NEW。
接着,调用start创建线程,当线程参与调度,这时,线程的状态就是RUNNABLE。
接着,主线程调用join方法,阻塞等待线程执行结束,当线程执行完run方法,线程就结束了,PCB就释放了,但是 t对象还在,这时,线程的状态就是TERMINATED
对于TERMINATED这个状态,我再啰嗦几句:
这个状态是不得已而存在的,是为了把 PCB已经释放的 线程对象 标识成无效。
当线程结束,内核中的PCB就释放了,但因为Java中对象的生命周期自有其规则,和PCB的生命周期并非完全一致。所以PCB释放时,无法保证Java代码中的 t对象也释放了。 那么就一定会存在,内核中的PCB已经释放,但代码中 t对象还存在的这种情况。
所以我们就需要一个特定的状态来把 t对象标识成无效,让程序员知道,该对象已经没用了。
避免出现线程已经结束了,但由于t对象还在,误以为线程还存在,还使用该线程进行多线程编程的情况。这就比较难受了。
所以,TERMINATED这个状态必须得存在,没有办法。
当线程的状态是TERMINATED时,就不能使用该线程进行多线程编程了。但是,对象还在,所以我们是可以调用对象中的方法和属性的,这并不冲突。
举例如下:(这些 对象的方法都可以调用,完全没问题)
2、线程在RNUUABLE状态下因为一些原因导致线程进入阻塞状态,即进入支线任务。当线程在执行run方法过程中,因为各种原因产生阻塞,这时,线程状态就会发生改变,就不会一直处于RUNNABLE状态了。
线程因为不同原因发生阻塞会进入不同的状态:
(1)TIMED_WAITING:因为需要等待一定的时间产生的阻塞,如sleep等类似方法。时间到了,就从该状态中出来,回到RUNNABLE状态了
(2)WAITING:因为需要等待其他人来通知产生的阻塞,如wait/join等方法。条件满足,通知到了,就从该状态中出来,回到RUNNABLE状态了
(3)BLOCKED:因为等待锁产生的阻塞。获取到锁,就从该状态中出来,回到RUNNABLE状态了
下面我通过代码来演示一下TIMED_WAITING状态:
线程的run方法中有个循环,循环10万次,循环内部有个sleep,休眠10毫秒。当线程运行i<10_0000这样的代码时,线程的状态就是RUNNABLE;当处于sleep阻塞时,线程的状态就是TIMED_WAITING。
主线程的main方法中也有个循环,循环1千次,循环内部打印线程的状态。由于这个循环在start方法之后,所以打印的线程状态,要么是RUNNABLE(当线程在CPU上执行或在就绪队列中时),要么是TIMED_WAITING(当线程在阻塞等待时)。获取的状态是什么,取决于获取的那一瞬间,线程处于哪个状态(正在CPU上执行或在就绪队列中,还是在阻塞等待)。
但是为啥会先打印RUNNABLE,再打印TIMED_WAITING,且TIMED_WAITING打印的更多呢?
因为循环在start方法之后,如上图,在主线程打印线程的状态时,线程先是在CPU上执行i<10_0000这样的代码,然后才进入sleep等待。这样的过程会循环10_0000次。所以,当主线程循环打印1000次线程的状态时,先打印的状态是RUNNABLE,后打印的状态是TIMED_WAITING。且因为sleep休眠10毫秒,虽然10ms对人来说很快,但对计算机来说还是很慢的。而线程在CPU上执行的代码却很少。线程在CPU上执行的时间远远小于线程sleep的时间,所以打印的TIMED_WAITING状态更多(进入sleep产生的阻塞就是TIMED_WAITING状态)。
三、多线程的意义到底是啥?
多线程最核心的地方:抢占式执行,随机调度
使用多线程编程能解决“并发编程”的问题,更好的利用了CPU的多核资源,提高执行速度。
下面我通过一个例子,让大家感受一下,单个线程和多个线程之间,执行速度的差别。
程序主要分成:CPU密集 和 IO密集 两种。CPU密集:包含了大量的加减乘除等算术运算。IO密集:涉及到读写文件,读写控制台,读写网络。
我们举一个CPU密集的例子,比如现在有一个运算量很大的任务,看一下单线程和多线程的区别。
假设当前有两个变量,需要把两个变量各自自增 100亿 次。
单线程(只有主线程):先对 a自增,再对 b自增。
两个线程(主线程和线程):线程先对 a自增,再对 b自增。
三个线程(主线程,线程1,线程2):线程1 和 线程2 分别对 a和b 自增。
加个计时的操作来衡量代码的执行速度,System.currentTimeMillis() —— 获取到当前系统的 ms级时间戳。
上述代码是一个线程完成的,只有主线程。
我们可以看到,执行时间大约是1万 ms
上述代码是两个线程完成的,主线程 和 线程t。在主线程中调用start方法创建一个线程,这个线程负责先对 a自增,再对 b自增 。
我们可以看到,执行时间大约是 5000 ms
上述代码是三个线程完成的,主线程,线程t1 和 线程t2。线程t1 和 线程t2 分别对 a和b 自增。
我们可以看到,执行时间大约 3000 ms
刚开始,我将计时代码:long begin = System.currentTimeMillis();写在了两个start方法之后,发现结果有问题。
为什么呢?为什么不能写在start后,要写在start前呢?
因为主线程,线程t1,线程t2 是并发执行的,CPU是随机调度的,哪个线程先去CPU上执行是随机的。如果把计时代码写在两个start 方法之后,在主线程还没执行到计时代码时,线程t1 或 线程t2 可能已经去执行run方法中的代码了。就不准了。总共的执行时间会被少算。
通过上面三个代码,观察发现:
只有主线程:10000 ms
主线程,线程t:5000 ms
主线程,线程t1,线程t2:3000 ms
总结:多线程能充分利用CPU的多核资源,提高运行效率。