一、线程的生命周期
线程的生命周期,就是一个线程从创建到消亡的过程。关于Java中线程的生命周期,首先看一下下面这张较为经典的图:
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种不同的状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。
新建状态(New)
创建Thread类的实例成功后,则该线程对象就处于新建状态。处于新建状态的线程有自己的内存空间,通过调用start()方法进入就绪状态(Runnable)。
就绪状态(Runnable)
处于就绪状态的线程已经具备了运行条件(也就是具备了在CPU上运行的资格),但还没有分配到CPU的执行权,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用run()方法。
运行状态(Running)
处于就绪状态的线程,如果获得了CPU的调度,就会从就绪状态变为运行状态,执行run()方中的任务。
运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
如果该线程失去了CPU资源,就会又从运行状态变为就绪状态,重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出CPU资源,再次变为就绪状态。
阻塞状态(Blocked)
在某种特殊的情况下,被人挂起或执行输入输出操作时,让出CPU执行权并临时中断自己的执行,从而进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态。当调用notify()或notifyAll()等方法,则该线程就会重新转入就绪状态。
- 同步阻塞:线程在获取同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。当获取同步锁成功,则该线程就会重新转入就绪状态。
- 其它阻塞:通过调用线程sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,则该线程就会重新转入就绪状态。
死亡状态(Dead)
线程在run()方法执行完了或者因异常退出了run()方法,该线程结束生命周期。此外,如果线程执行了interrupt()或stop()方法,那么它也会以异常退出的方式进入死亡状态。
二、线程状态的控制
Java提供了一些便捷的方法用于会线程状态的控制。线程状态的控制Java给我们提供了很多方法,但是有些已经标注为过时的,我们应该尽可能的避免使用它们,此处我们重点关注start()、join()、sleep()、yield()等直接控制方法,和setDaemon()、setPriority()等间接控制方法。
线程睡眠
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,指定时间之后,解除阻塞状态,进入就绪状态,则可以通过调用Thread的sleep()方法来实现。从API查阅可以看到sleep()方法有两种重载的形式,但是使用方式一模一样。
比如,我们想要使主线程每休眠1000毫秒,然后再打印出数字:
【示例】每隔1000毫秒打印一个数字
public class Test { public static void main(String[] args) { for(int i = 1; i < 10; i++) { System.out.println(i); try { Thread.sleep(1000); // 对主线程休眠1秒钟 } catch (InterruptedException e) { e.printStackTrace(); } } } }
通过执行代码,我们可以明显看到打印的数字在时间上有些许的间隔。
注意事项:
- sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。
- 使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。
线程的优先级
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,能够设置[1-10]之间的整数,数值越大,那么优先级越高,也可以使用Thread类提供的三个静态常量:
4. public final static int MIN_PRIORITY = 1;
5. public final static int NORM_PRIORITY = 5; 默认
6. public final static int MAX_PRIORITY = 10;
【示例】测试线程优先级的执行
/** * 自定义线程类 */ class TestThread extends Thread { public TestThread() {} public TestThread(String name, int pro) { super(name); // 设置线程名字 this.setPriority(pro); // 设置线程的优先级 } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); } } } /** * 测试类 */ public class Test { public static void main(String[] args) { new TestThread("高级", 10).start(); new TestThread("低级", 1).start(); } }
执行程序,从结果可以看到,一般情况下,高级线程更先执行完毕。
线程让步
yield()方法和sleep()方法有点相似,yield()方法也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出CPU资源给其它的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。
实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉CPU调度线程。
【示例】线程让步的使用 /** * 自定义线程类 */ class TestThread extends Thread { public TestThread() {} public TestThread(String name, int pro) { super(name); // 设置线程名字 this.setPriority(pro); // 设置线程的优先级 } @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); Thread.yield(); // 线程让步 } } } /** * 测试类 */ public class Test { public static void main(String[] args) { new TestThread("高级", 10).start(); new TestThread("低级", 1).start(); } }
关于sleep()方法和yield()方的区别如下:
sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。 sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。 sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行,因此开发中yield方法不常用。
线程合并
线程的合并就是:线程A在运行期间,可以调用线程B的join()方法,这样线程A就必须等待线程B执行完毕后,才能继续执行A线程。
应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。
线程合并有三个重载的方法:
【示例】线程合并的使用
/** * 自定义线程类 */ class TestThread extends Thread { public TestThread() {} public TestThread(String name, int pro) { super(name); // 设置线程名字 this.setPriority(pro); // 设置线程的优先级 } @Override public void run() { for (int i = 0; i < 30; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); } } } /** * 测试类 */ public class Test { public static void main(String[] args) { TestThread th = new TestThread(); th.start(); try { // 等待th线程执行任务完毕之后,再执行主线程中的任务 th.join(); } catch (InterruptedException e) { e.printStackTrace(); } // 主线程任务 for (int i = 0; i < 30; i++) { String name = Thread.currentThread().getName(); System.out.println(name + "线程第" + i + "次执行!"); } } }
在这个例子中,在主线程中调用th.join(); 就是将主线程加入到th子线程后面等待执行。
守护线程
守护线程与普通线程写法上基本没啥区别,调用线程对象的方法setDaemon(true),就可以把该线程标记为守护线程。
当普通线程(前台线程)都全部执行完毕,也就是当前在运行的线程都是守护线程时,Java虚拟机(JVM)将退出。另外,setDaemon(true)方法必须在启动线程前调用,否则抛出IllegalThreadStateException异常。
【示例】线程合并的使用
// 前台线程 class CommonThread extends Thread { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("前台线程第" + i + "次执行!"); } } } // 后台线程或守护线程 class DaemonThread extends Thread { int i = 0; @Override public void run() { while(true) { System.out.println("后台线程第" + i++ + "次执行!"); } } } //测试类 public class Test { public static void main(String[] args) { CommonThread ct = new CommonThread(); DaemonThread dt = new DaemonThread(); ct.start(); dt.setDaemon(true); // 将dt设置为守护线程 dt.start(); } }
运行以上代码,通过执行的结果可以看出:前台线程是保证执行完毕的,后台线程还没有执行完毕就退出了。
守护线程的用途:守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。