1 线程的生命周期
当线程被创建并启动以后,它既不是一启动就进入执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直霸占着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。
1.1 新建和就绪状态
新建状态:当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由JVM为其分配内存,并初始化其成员变量的值。
就绪状态:当线程对象调用了start()方法之后,该线程处于就绪状态,JVM会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于何时运行,取决于JVM里线程调度器的调度。
注意:
(1)启动线程使用start()方法,而不是run()方法!永远不要调用线程对象的run()方法!调用start()方法来启动线程,系统会把对应的run()方法当做线程执行体来处理,但如果直接调用线程对象的run()方法,则run()方法立即就会执行,而且在run()方法返回之前其他线程无法并发执行。简而言之,就是启动线程的正确方法是调用Thread对象的start()方法,而不是直接调用run()方法,否则就不是新开线程了,而是在同步单线执行了。
(2)只能对处于新建状态的线程调用start()方法,否则将引发IllegalThreadStateException异常。
1.2 运行和阻塞状态
运行状态:如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。
当一个线程开始运行后,它很难一直处于运行状态,线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。说明对于抢占式策略,在选择下一个线程时,系统会考虑线程的优先级。
所有现代的桌面和服务器操作系统都采用抢占式调度策略,但一些小型设备比如手机则可能采用协作式调度策略,在这样的系统中,只有当一个线程调用了它的sleep()或yield()方法后才会放弃所占用的资源-也就是必须由该线程主动放弃所占用的资源。
当发生如下情况时,线程将会主动进入阻塞状态:
(1)线程调用sleep()方法主动放弃所占用的处理器资源。
(2)线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
(3)线程试图获得一个同步锁,但该同步锁被其他线程所持有。
(4)线程在等待某个通知(notify)。
(5)程序调用了线程的suspend()方法将线程挂起。(容易引起死锁,应尽量避免使用!)
针对上面的几种情况,就当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:
(1)调用sleep()方法的线程经过了指定的时间。
(2)线程调用的阻塞式IO方法已经返回。
(3)线程成功获得了同步锁。
(4)线程正在等待某个通知时,其他线程发出了一个通知。
(5)处于挂起状态的线程被调用了resume()恢复方法。
线程状态转换图
从上图中也可以看到,线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定,当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态。但有一个方法例外,调用yield()方法可以让运行状态的线程转入就绪状态。
1.3 线程死亡
以下三种方式结束,线程即进入死亡状态:
(1)run()或call()方法执行完成,线程正常结束。
(2)线程抛出一个未捕获的Exception或Error。
(3)直接调用该线程的stop()方法来结束该线程,该方法容易导致死锁,不推荐使用。
注意:不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡就是死亡,该线程将不可再次作为线程执行,否则将引发IllegalThreadStateException异常。
2 控制线程
2.1 join线程
join()方法:让一个线程等待另一个线程完成。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。
2.2 后台线程
后台线程也叫守护线程,JVM的垃圾回收线程就是典型的后台线程。后台线程有个特征,如果所有的前台线程都死亡,后台线程也会自动死亡。通过调用Thread对象的setDaemon(true)方法可以将指定线程设置成后台线程,同时Thread类还提供了一个isDaemon()方法用于判断执行的线程是否为后台线程。
注意:将一个线程设置为后台线程,必须要在该线程启动之前设置,也就是说,setDaemon(true)方法必须在start()方法之前调用,否则将引发IllegalThreadStateException异常。
2.3 线程睡眠:sleep
线程调用sleep()方法进入阻塞状态,在其睡眠时间段内,该线程不会获得执行机会,即便系统中没有其他可执行的线程,处于sleep()中的线程也不会执行。
2.4 线程让步:yield
yield()方法可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是,当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。
实际上,当某个线程调用yield()方法之后,只有优先级与当前线程相同或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。在多CPU并行的环境下,yield()方法的功能很多时候并不明显。
2.5 改变线程优先级
每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。每个线程默认的优先级都与创建它的父线程的优先级相同。
注意:由于不同操作系统的支持不同,应该尽量避免直接为线程指定优先级,而应该使用静态常量来设置优先级,这样可以保证程序具有最好的可移植性。