当线程并创建并启动后,它既不是一启动就进入执行状态,也不是一直处于执行状态,再次线程的生命周期中,他要经过新建(New)、就绪(Ready)、阻塞(Blocked)和死亡(Dead)5种状态。
一、新建和就绪状态
1.1 新建状态
当程序使用new关键字创建了一个线程后,该线程就处于新建状态,此时它与其他Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时线程对象没有表现出任何动态特征,程序也不会执行线程的线程执行体。
1.2 就绪状态
当程序调用start()方法后,该程序处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计算器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。
1.3 注意事项
注意:启动线程使用start()方法,而不是run()方法!永远不要调用线程对象的run()方法!调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果直接用线程的run()方法,则run()方法立即会被执行,而且再run()方法返回之前其他线程无法并发执行——也就是说,如果直接调用run()放啊,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程的执行体
package section3;
public class InvokeRun extends Thread
{
private int i;
//重写run()方法
public void run()
{
for(;i<100;i++)
{
//直接调用run()方法时,Thread的getName()返回的就是该对象的名字
//而不是当前线程的名字
//使用Thread.currentThread().getName()总是获取当前线程的名字
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args)
{
for(var i=0;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20)
{
//直接调用线程的run()方法,系统会把线程对象当成普通对象,把run()方法当成普通方法
//所以下面两个方法不会启动线程,而是依次执行两个Run()方法
new InvokeRun().run();
new InvokeRun().run();
}
}
}
}
上面程序创建线程对象后直接调用线程run()方法,程序运行的结果是整个程序只有一个线程:主线程。如果直接调用线程的run()方法,则run()方法不能直接通过getName()方法来获取当前执行线程的名字,而是需要使用Thread.currentThread()方法先获取当前线程对象,再调用线程对象的getName()方法来获取当前线程的名字。
调用线程的run()方法后,该线程已经不再处于新建状态,不要再次调用线程对象的start()的方法。
调用线程对象的start()方法之后,该线程就会立即进入就绪状态——就绪状态就是“等待执行”,该线程并未真正进入运行状态。
将上面的程序中的new InvokeRun().run();改为new InvokeRun().start();后重新运行上面的程序,可以看到以下输出:
可以看出等到主线程的成员变量i=46后才启动子线程,这种切换由底层平台控制(具有一定随机性)。
如果希望调用子线程的start()方法后子线程立即执行,程序可以使用Thread.sllep(1)来让当前运行的线程睡眠1毫秒——这1毫秒CPU不会空闲,他会去执行另一个处于就绪状态的的线程,这样可以让子线程立即开始运行。
二、运行和阻塞状态
2.1 运行状态
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,该线程处于运行状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。当然,在一个多处理器的计算机上,将会由多个线程并行执行;当线程数大于处理器数,依然会存在多个线程在同一个CPU上轮换的现象。
当线程开始运行后,它不会一直处于运行状态,线程在运行的过程中需要被抢断,目的是使其他线程获得执行的机会。对于抢占式策略的系统而言,系统会给每一个线程一个先小时间段来处理任务;当该时间段用完,系统就会剥夺该线程所占的资源,让其他线程获得执行机会。在选择下一个线程时,系统会考虑线程的优先级。
对于一些小型设备如手机可能采用协作式调度策略,在这样的系统中,只有当一个线程调用它的sleep()或yield()方法后才会放弃所占用的资源——也就是必须由线程主动放弃所占有的资源。
2.2 阻塞状态
如果发生以下情况时,线程将会进入阻塞状态
1、线程调用sleep()方法主动放弃所占有的处理器资源。
2、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
3、线程视图获取一个同步监听器,但该同步监听器正被其他线程所持有。
4、线程在等待某个通知(notify)。
5、程序调用线程的suspend()方法将该线程挂起。但该方法容易导致死锁,所以应该尽量避免使用该方法。
当正在执行的线程被阻塞之后,其他线程就可以获得执行机会。被阻塞的线程就会再合适的时候重新进入就绪状态。也就是说,阻塞线程的阻塞接触之后,必须重新等待线程调度器再次调度它。
正对上面的情况,当发生如下特点的情况可以解除上面的阻塞,让线程重新进入就绪状态。
1、调用sleep()方法的线程经过了指定时间。
2、线程调用阻塞式IO方法已返回。
3、线程成功获取了视图取得的同步监听器
4、线程正在等待某个通知,其他线程发了一个通知。
5、处于挂起状态的线程被调用了resume()恢复方法。
下图展示了线程状态的转换图:
从上图可以看出,线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。而就绪而运行状态之间的的装欢不受程序控制,而是由系统线程调度决定的,当处于就绪状态的线程获得了处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态。但有一个方法例外,调用yield()放啊可以让运行状态的线程进入就绪状态。
三、线程死亡
3.1 线程结束的三种方式
线程会以以下三种方式之一结束,一个结束后就处于死亡状态:
1、run()方法执行完成,线程正常结束。
2、线程抛出一个未捕获的 Exception或Error。
3、直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。
注意:当主线程结束时,其他线程不受任何影响,并不会随之结束。。一旦子线程启动起来,它就拥有和主线程相同的地位,它不受主线程的影响。
3.2 判断线程死亡的方法
isAlive():当线程处于就绪、运行、阻塞三种状态时,该方法将返回ture;当线程处于新建、死亡两种状态时,该方法将返回false。
3.3 代码举例
package section3;
public class StartDead extends Thread
{
private int i;
public void run()
{
for (;i<100;i++)
{
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args)
{
//创建线程
var sd=new StartDead();
for(var i=0;i<100;i++)
{
//调用Thread的currentThread()方法获取当前线程
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20)
{
//启动线程
sd.start();
//判断线程的isAlive()值,输出true
System.out.println(sd.isAlive());
}
//当线程处于新建、死亡两种状态时,isAlive()就返回false
//当i大于20时,该线程已经启动过了,如果sd.Alive()返回false
//那就是死亡状态
if(i>20 &&!sd.isAlive())
{
//视图再次启动线程
sd.start();
}
}
}
}
运行上面的程序,在线程已经死亡的状态下再次调用start()方法来启动该线程,将会引发IllegalThreadStateException异常,这表明处于死亡状态的线程无法再次运行了。