线程状态
线程状态
线程与进程
线程:进程中负责程序执行的执行单元。线程本身依靠程序进行运行。线程是程序中的顺序控制流,只能使用分配给程序的资源和环境。
进程:执行中的程序。一个进程至少包含一个线程。
单线程:程序中只存在一个线程,实际上主方法就是一个主线程。
多线程:在一个程序中运行多个任务,目的是更好地使用CPU资源。
线程的状态
线程的状态有以下几种:
创建状态(new):准备好了一个多线程的对象
就绪状态(runnable):调用了start()方法,等待CPU进行调度
运行状态(running):执行run()方法
阻塞状态(blocked):暂时停止执行,可能将资源交给其它线程使用
终止状态(dead):线程销毁
当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源),只有线程运行需要的所有条件满足了,才进入就绪状态。
当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其它的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。
线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的时间)、waiting(等待被唤醒)、blocked(阻塞)。
当由于突然中断或者子任务执行完毕,线程就会被消亡。
下面这幅图描述了线程从创建到消亡之间的状态:
有些教程将blocked、waiting、time waiting统称为阻塞状态,这个也是可以的。这里想将线程的状态和Java中的方法调用联系起来,所以将waiting和time waiting两个状态分离出来。
【注】sleep和wait的区别:
sleep是Thread类的方法,wait是Object类中定义的方法。
Thread.sleep不会导致锁行为的改变,如果当前线程是拥有锁的,那么Thread.sleep不会让线程释放锁。
Thread.sleep和Object.wait都会暂停当前的线程。OS会将执行时间分配给其它线程。区别是,调用wait后,需要别的线程执行notify/notifyAll才能够重新获得CPU执行时间。
上下文切换
对于单核CPU来说(相对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转而去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。
由于当前线程的任务可能并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:线程A正在读取一个文件的内容,正读到文件的一般,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。
因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值是多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
简单的说:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。
线程的常用方法
方法 | 说明 |
---|---|
public void start() |
使该线程开始执行;Java虚拟机调用该线程的run()方法。 |
public void run() |
如果该线程是使用独立的Runnable运行对象构造的,则调用该Runnable对象的run方法;否则,该方法不执行任何操作并返回。 |
public final void setName(String name) |
改变线程名称,使之与参数name相同。 |
public final void setPriority(int priority) |
更改线程的优先级。 |
public final void setDaemon(boolean on) |
将该线程标记为守护线程或用户线程。 |
public final void join(long millisec) |
等待该线程终止的时间最长为millis毫秒。 |
public void interrupt() |
中断线程。 |
public final boolean isAlive() |
测试线程是否处于活动状态。 |
public static void yield() |
暂停当前正在执行的线程对象,并执行其它线程。 |
public static void sleep(long millisec) |
在指定的毫秒数内让正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
public static Thread currentThread() |
返回当前正在执行的线程对象的引用。 |
Thread类中的方法调用引起线程状态变化的说明如下图:
停止线程
停止线程是在多线程开发时很重要的技术点,掌握此技术可以对线程的停止进行有效地处理。
停止一个线程可以使用Thread.stop()方法,但最好不要用。该方法是不安全地,已被弃用。
在Java中有下列3种方法可以终止正在运行地线程:
使用退出标志,使线程正常退出,也就是当run()方法完成后线程终止。
使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样,都是作废过期地方法,使用它们可能产生不可预料地结果。
使用interrupt方法中断线程,但这个不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。
暂停线程
使用interrupt()方法。
守护线程
在Java线程中有两种线程,一种是User Thread(用户线程),另一种是Daemon Thread(守护线程)。
Daemon的作用是为其它线程的运行提供服务,比如说GC线程。其实User Thread和Daemon Thread本质上来说没什么区别,唯一的区别之处在于虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没线程可以服务的了,所以虚拟机也就退出了。
守护线程并非虚拟机内部才可以提供,用户也可以自行设定守护线程,使用方法 public final void setDeamon(boolean on);
但是有几点需要注意:
threadsetDaemon(true)必须在thread.start()之前设置,否则会抛出IllegalThreadStateException异常。不能把正在运行的常规线程设置为守护线程。(这点与守护进程有者明显的区别,守护进程是创建后,让进程摆脱原会话的控制、让进程摆脱原进程组的控制、让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有者本质上面的区别。)
在守护线程种产生的新线程也是Deamon的。(这点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的父进程不是init进程,所谓的守护进程本质上说就是 **”父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/“**。)
不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来得及进行操作时,虚拟机可能已经退出了。