# 线程状态

引用自Java 多线程干货系列(1)

# 线程与进程

  1. 线程:进程中负责程序执行的执行单元。线程本身依靠程序进行运行。线程是程序中的顺序控制流,只能使用分配给程序的资源和环境。

  2. 进程:执行中的程序。一个进程至少包含一个线程。

  3. 单线程:程序中只存在一个线程,实际上主方法就是一个主线程。

  4. 多线程:在一个程序中运行多个任务,目的是更好地使用CPU资源。

# 线程的状态

线程的状态有以下几种:

  • 创建状态(new):准备好了一个多线程的对象

  • 就绪状态(runnable):调用了start()方法,等待CPU进行调度

  • 运行状态(running):执行run()方法

  • 阻塞状态(blocked):暂时停止执行,可能将资源交给其它线程使用

  • 终止状态(dead):线程销毁

当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源),只有线程运行需要的所有条件满足了,才进入就绪状态。

当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其它的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。

线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的时间)、waiting(等待被唤醒)、blocked(阻塞)。

当由于突然中断或者子任务执行完毕,线程就会被消亡。

下面这幅图描述了线程从创建到消亡之间的状态:

线程的生命周期.jpg

有些教程将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类中的方法调用引起线程状态变化的说明如下图:

线程状态的变化.jpg

  • 停止线程

    停止线程是在多线程开发时很重要的技术点,掌握此技术可以对线程的停止进行有效地处理。

    停止一个线程可以使用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还没来得及进行操作时,虚拟机可能已经退出了。