线程的状态(系统层面)
一个线程被创建后就进入了线程的生命周期。在线程的生命周期中,共包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)这五种状态。当线程启动以后,CPU需要在多个线程之间切换,所以线程也会随之在运行、阻塞、就绪这几种状态之间切换。
线程的状态转换如图:
当一个正在运行的线程遇到如下情况时,线程会从运行态转为阻塞态:
① 线程调用sleep、join等方法。
② 线程调用了一个阻塞式IO方法。
③ 线程试图获得一个同步监视器,但是该监视器正在被其他线程持有。
④ 线程在等待某个 notify 通知。
⑤ 程序调用了线程的suspend方法将该线程挂起。
当线程被阻塞后,其他线程就有机会获得CPU资源而被执行。当上述导致线程被阻塞的因素解除后,线程会回到就绪状态等待处理机调度而被执行。
当一个线程执行结束后,该线程进入死亡状态。
有以下3种方式可结束一个线程:
① run 方法执行完毕。
② 线程抛出一个异常或错误,而该异常或错误未被捕获。
③ 调用线程的 stop方法结束该线程。(不推荐使用)
线程的控制
Thread类中提供了一些控制线程的方法,通过这些方法可以轻松地控制一个线程的执行和运行状态,以达到程序的预期效果。
join 方法
如果线程A调用了线程B的join方法,线程A将被阻塞,等待线程B执行完毕后线程A才会被执行。这里需要注意一点的是,join方法必须在线程B的start方法调用之后调用才有意义。join方法的主要作用就是实现线程间的同步,它可以使线程之间的并行执行变为串行执行。
join 方法有以下3种重载形式:
① join(): 等待被join的线程执行完成。
② join(long millis): 等待被join 的线程的时间为 millis 毫秒,如果该线程在millis 毫秒内未结束,则不再等待。
③ join(long millis,int nanos): 等待被join的线程的时间最长为 millis 毫秒加上nanos微秒。
public class JoinThread extends Thread{ @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName() + "---" + i); try { Thread.sleep(500); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
public class TestThreadState { public static void main(String[] args) { // 创建要加入当前线程的线程,并启动 JoinThread j1 = new JoinThread(); j1.start(); // 加入当前线程,阻塞当前线程,直到加入线程执行完毕 try { j1.join(); } catch (InterruptedException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName()); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
我们定义了一个JoinThread类,它继承了Thread类,这是我们要加入的线程类。
在main方法中,我们创建了JoinThread线程,并把它加入到当前线程(主线程)中,并没有指定当前线程等待的时间,所以会一直阻塞当前线程,直到JoinThread线程的run方法执行完毕,才会继续执行当前线程。
sleep 方法
当线程A调用了 sleep方法,则线程A将被阻塞,直到指定睡眠的时间到达后,线程A才会重新被唤起,进入就绪状态。
sleep方法有以下2种重载形式:
① sleep(long millis):让当前正在执行的线程暂停millis毫秒,该线程进入阻塞状态。
② sleep(long mills,long nanos):让当前正在执行的线程暂停 millis 毫秒加上 nanos微秒。
public class Test { public static void main(String[] args) { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "---" + i); try { Thread.sleep(1000); // 阻塞当前线程1s } catch (Exception e) { e.printStackTrace(); } } } }
这段代码中没有创建其他线程,只有当前线程存在,也就是执行main函数的主线程。for循环中每打印一次线程名称,主线程就会被sleep方法阻塞1s,然后进入就绪状态,重新等待被调到,实现了线程的控制。
yield 方法
当线程A调用了yield方法,它可以暂时放弃处理器,但是线程A不会被阻塞,而是进入就绪状态。
public class YieldThread extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "---" + i); // 主动放弃 Thread.yield(); } } }
我们自定义了一个线程类YieldThread,在run方法中定义了一个for循环,for循环中每打印一次线程名称,就会调用一下yield方法,主动放弃CUP让给其它有相同优先级或更高优先级的线程,自己进入就绪状态,等待被CPU调度。
设置线程的优先级
每个线程都有自己的优先级,默认情况下线程的优先级都与创建该线程的父线程的优先级相回。同时Thread类提供了setPriority(int priority) 和getPriority()方法设置和返回指定线程的优先级。参数priority是一个整型数据,用以指定线程的优先级。priority 的取值范围是1-10,默认值为5,也可以使用Thread类提供的三个静态常量设置线程的优先级。
① MAX_PRIORITY:最高优先级,其值为10。
② MIN_PRIORITY:最低优先级,其值为1。
③ NORM_PRIORITY:普通优先级,其值为5。
public class TestThreadPriority { public static void main(String[] args) { // 线程优先级 ThreadPriority p1 = new ThreadPriority(); p1.setName("p1"); ThreadPriority p2 = new ThreadPriority(); p2.setName("p2"); ThreadPriority p3 = new ThreadPriority(); p3.setName("p3"); p1.setPriority(1); p3.setPriority(10); p1.start(); p2.start(); p3.start(); } }
我们创建了三个线程p1、p2、p3,设置了p1的优先级为1,p3的优先级为10,并没有设置p2的,所以p2的优先级默认是5。优先级越高,表示获取cup的机会越多,注意此处说的是机会,所以高优先级的线程并不是一定先于低优先级的线程被CPU调度,只是机会更大而已。
sleep方法和wait方法的区别是什么?
sleep 方法是Thread类的一个静态方法,其作用是使运行中的线程暂时停止指定的毫秒数,从而该线程进入阻塞状态并让出处理器,将执行的机会让给其他线程。但是这个过程中监控状态始终保持,当sleep的时间到了之后线程会自动恢复。
wait 方法是Object类的方法,它是用来实现线程同步的。当调用某个对象的wait方法后,当前线程会被阻塞并释放同步锁,直到其他线程调用了该对象的 notify 方法或者 notifyAll 方法来唤醒该线程。所以 wait 方法和 notify(或notifyAll)应当成对出现以保证线程间的协调运行。
sleep方法和yield方法的区别是什么?
① sleep方法暂停当前线程后,会给其他线程执行机会而不会考虑其他线程的优先级。但是yield方法只会给优先级相同或者优先级更高的线程执行机会。
② sleep方法执行后线程会进入阻塞状态,而执行了yield方法后,当前线程会进入就绪状态。
③ 由于sleep方法的声明抛出了 InterruptedException 异常,所以在调用sleep方法时需要catch 该异常或抛出该异常,而yield 方法没有声明抛出异常。
④ sleep 方法比yield 方法具有更好的可移植性。
补充一下sleep、yield、join和wait的差异:
① sleep、join、yield时并不释放对象锁资源,在wait操作时会释放对象资源,wait在被notify/notifyAll
唤醒时,重新去抢夺获取对象锁资源。
② sleep、join、yield可以在任何地方使用,而wait,notify,notifyAll只能在同步控制方法或者同步控制块中使用。
③ 调用wait会立即释放锁,进入等待队列,但是notify()不会立刻释放sycronized(obj)中的对象锁,必须要等notify()
所在线程执行完sycronized(obj)
同步块中的所有代码才会释放这把锁,然后供等待的线程来抢夺对象锁。
Java中为什么不建议使用stop和suspend方法终止线程?
在Java中可以使用stop 方法停止一个线程,使该线程进入死亡状态。但是使用这种方法结束一个线程是不安全的,在编写程序时应当禁止使用这种方法。
之所以说stop方法是线程不安全的,是因为一旦调用了Thread.stop()方法,工作线程将抛出一个ThreadDeath的异常,这会导致run方法结束执行,而且结束的点是不可控的,也就是说,它可能执行到run方法的任何一个位置就突然终止了。同时它还会释放掉该线程所持有的锁,这样其他因为请求该锁对象而被阻塞的线程就会获得锁对象而继续执行下去。一般情况下,加锁的目的是保护数据的一致性,然而如果在调用Thread.stop()后线程立即终止,那么被保护数据就有可能出现不一致的情况(数据的状态不可预知)。同时,该线程所持有的锁突然被释放,其他线程获得同步锁后可以进入临界区使用这些被破坏的数据,这将有可能导致一些很奇怪的应用程序错误发生,而且这种错误非常难以debug.所以在这里再次重申,不要试图用stop 方法结束一个线程。
suspend方法可以阻塞一个线程,然而该线程虽然被阻塞,但它仍然持有之前获得的锁,这样其他任何线程都不能访问相同锁对象保护的资源,除非被阻塞的线程被重新恢复。如果此时只有一个线程能够恢复这个被suspend的线程,但前提是先要访问被该线程锁定的临界资源。这样便产生了死锁。所以在编写程序时,应尽量避免使用suspend,如确实需要阻塞一个线程的运行,最好使用wait方法,这样既可以阻塞掉当前正在执行的线程,同时又使得该线程不至于陷入死锁。
用一句话说就是:stop方法是线程不安全的,可能产生不可预料的结果;suspend方法可能导致死锁。
如何终止一个线程?
在Java 中不推荐使用stop方法和suspend方法终止一个线程,因为那是不安全的,那么要怎样终止一个线程呢?
方法一:使用退出标志
正常情况下,当Thread 或 Runnable 类的run方法执行完毕后该线程即可结束,但是有些情况下run方法可能永远都不会停止,例如,在服务端程序中使用线程监听客户端请求,或者执行其他需要循环处理的任务。这时如果希望有机会终止该线程,可将执行的任务放在一个循环中(例如 while循环),并设置一个boolean型的循环结束的标志。如果想使 while 循环在某一特定条件下退出,就可以通过设置这个标志为true或false 来控制 while 循环是否退出。这样将线程结束的控制逻辑与线程本身逻辑结合在一起,可以保证线程安全可控地结束。
让我们来看一看案例:
public class TestQuitSign { // 退出标志 public static volatile boolean quitFlag = false; // 退出标志:针对运行时的线程 public static void main(String[] args) { // 线程一:每隔一秒,打印一条信息,当quitFlag为true时结束run方法。 new Thread() { public void run() { System.out.println("thread start..."); while (!quitFlag) { try { Thread.sleep(1000); } catch (Exception e) { } System.out.println("thread running..."); } System.out.println("thread end..."); } }.start(); // 线程二:等待三秒,设置quitFlag为true,终止线程一。 new Thread() { public void run() { try { Thread.sleep(3000); } catch (Exception e) { // TODO: handle exception } quitFlag = true; } }.start(); } }
在上面这段程序中的main方法里创建了两个线程,第一个线程的run方法中有一个while循环,该循环通过boolean型变量quitFlag控制其是否结束。因为变量quitFlag的初始值为false,所以如果不修改该变量,第一个线程中的run方法将不会停止,也就是说,第一个线程将永远不会终止,并且每隔1s在屏幕上打印出一条字符串。第二个线程的作用是通过修改变量quitFlag来终止第一个线程。在第二个线程的run方法中首先将线程阻塞3s,然后将quitFlag置为true.因为变量quitFlag是同一进程中两个线程共享的变量,所以可以通过修改quitFlag的值来控制第一个线程的执行。当变量quitFlag被置为true,第一个线程的while循环就可以终止,所以run方法就能执行完毕,从而安全退出第一个线程。
注意,boolean 型变量 quitFlag 被声明为 volatile,volatile 会保证变量在一个线程中的每一步操作在另一个线程中都是可见的,所以这样可以确保将 quitFlag 置为true 后可以安全退出第一个线程。
方法二:使用 interrupt方法
使用退出线程标志的方法终止一个线程存在一定的局限性,主要的限制就是这种方法只对运行中的线程起作用,如果该线程被阻塞(例如,调用了 Thread.join()方法或者Thread.sleep()方法等)而处于不可运行的状态时,则退出线程标志的方法将不会起作用。
在这种情况下,可以使用Thread 提供的 interrupt()方法终止一个线程。因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,然后通过catch块捕获该异常,从而安全地结束该线程。
我们来看看下面的例子:
public class TestInterrupt { // Interrupt方法: 针对阻塞状态的线程 public static void main(String[] args) throws InterruptedException{ // 创建线程 Thread thread = new Thread() { public void run() { System.out.println("thread start..."); try { Thread.sleep(10000); } catch (InterruptedException e) { // 捕获中断异常 e.printStackTrace(); } System.out.println("thread end..."); } }; // 启动线程 thread.start(); // 主线程等待1秒,抛出一个中断信号 Thread.sleep(1000); thread.interrupt(); } }
在上面这段程序中的main方法里创建了一个线程,在该线程的 run 方法中调用 sleep 函数将该线程阻塞10s.然后调用Thread 类的 start 方法启动该线程,该线程刚刚被启动就进入阻塞状态。主线程等待1s后调用thread.interrupt()抛出一个中断信号,在run方法中的catch会正常捕获到这个中断信号,这样被阻塞的该线程就会提前退出阻塞状态,不需要等待10s线程thread 就会被提前终止。
上述方法主要针对当前线程调用了Thread.join()或者 Thread.sleep()等方法而被阻塞时终止该线程。如果一个线程被I/O阻塞,则无法通过thread.interrupt()抛出一个中断信号而离开阻塞状态。这时可推而广之,触发一个与当前I/O0阻塞相关的异常,使其退出I/O阻塞,然后通过catch 块捕获该异常,从而安全地结束该线程。
线程的状态(JVM层面)
我们在上面讨论的线程状态是从操作系统层面来看的,这样看比较直观,也容易理解,也是一个线程在操作系统中真实状态的体现。下面我们来看看Java 中线程的状态及转换。
Java 线程状态
在Java中线程的状态有6种,我们来看一看JDK 1.8帮助文档中的说明:
我们可以看到帮助文档中的最后一行,这些状态是不反映任何操作系统线程状态的JVM层面的状态。我们来具体看一看这六种状态:
NEW
:初始状态,线程被创建,但是还没有调用 start 方法。
RUNNABLED
:运行状态,JAVA 线程把操作系统中的就绪和运行两种状态统称为“运行状态”。
BLOCKED
:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了 CPU 使用权,阻塞也分为几种情况 :
-
等待阻塞:运行的线程执行了
Thread.sleep
、wait()
、join()
等方法JVM 会把当前线程设置为等待状态,当 sleep 结束、join 线程终止或者wait线程被唤醒后,该线程从等待状态进入到阻塞状态,重新抢占锁后进行线程恢复; -
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么
jvm
会把当前的线程放入到锁池中 ; -
其他阻塞:发出了 I/O请求时,JVM 会把当前线程设置为阻塞状态,当 I/O处理完毕则线程恢复;
WAITING
:等待状态,没有超时时间,要被其他线程唤醒或者有其它的中断操作;
- 执行 wait()
- 执行 join()
- 执行 LockSupport.park()
TIME_WAITING
:超时等待状态,超时以后自动返回;
- 执行 sleep(long)
- 执行 wait(long)、join(long)
- 执行 LockSupport.parkNanos(long)、LockSupport.parkUntil(long)
TERMINATED
:终止状态,表示当前线程执行完毕 。
Java 线程状态转换
总结
操作系统层面:
有5个状态,分别是:New(新建)、Runnable(就绪)、Running(运行)、Blocked(阻塞)、Dead(死亡)。
JVM层面:
有6个状态,分别是:NEW(新建)、RUNNABLE(运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(超时等待)、TERMINATED(终止)。
主要由这几个方法来控制:sleep、join、yield、wait、notify以及notifyALL。