虽然Thread类实现了Runnable接口,但是操作线程的主要方法并不在Runnable接口中,多线程操作,主要是通过Thread类中的方法实现。
线程的生命周期
要想实现多线程,必须在主线程中创建新的线程对象。任何线程一般具有5种状态:创建、就绪、运行、阻塞、终止。
线程状态的变化与方法之间的关系如下图:
![b0dc5e3407d57dbc70f9af95ba29be36.png](https://i-blog.csdnimg.cn/blog_migrate/95cbf7957ea627712819bd50f67d5503.jpeg)
01、创建状态(NEW)
新建一个线程对象后,新的线程对象就处于新建状态,此时它处于不可运行状态。新建一个线程对象使用Thread类的构造方法来实现。
Thread Thread = new Thread();
02、就绪状态(RUNNABLE)
新建的线程对象调用了线程的start方法就可以启动线程,当线程启动后,线程就进入就绪状态。此时,线程进入线程队列排队,等待CPU的调度。
03、运行状态(RUNNING)
当就绪状态的线程被CPU调用,并获得处理器资源时,线程就进入了运行状态。此时,系统会自动调用该线程对象的run方法。run方法定义了该线程的具体操作和功能。
04、阻塞状态(BLOCKED)
一个正在运行的线程在被人为挂起或者需要执行耗时的输入、输出操作时,将让出CPU资源,并暂时中止该线程的执行,进入阻塞状态。
在线程运行状态下,如果人为调用sleep方法、suspend方法、wait方法,线程就会进入阻塞状态,阻塞的线程会进入阻塞队列,只有当引起阻塞的事件解除后,线程才可以进入就绪状态,继续等待CPU的调度。
05、死亡状态(TERMINATED)
线程调用stop方法或者run方法执行结束后,即处于死亡状态,处于死亡状态的线程不再具有继续运行的能力,即该线程对象终止了。
线程操作的八个方法
线程的创建是用Thread类的构造方法,因此线程操作的主要方法是Thread类中的方法。
01、设置和获取线程名称
除了可以通过Thread类的构造方法设置线程名称之外,还可以通过setName方法设置线程名称。
获取当前线程名称的代码:
Thread.currentThread().getName();
示例1:在不自定义线程名称时,观察线程的默认名称的规律
public class Task implements Runnable {@Overridepublic void run() {for (int i = 1; i <= 3; i++) { System.out.println(Thread.currentThread().getName() + "运行,i=" + i); } }}// 测试类public class TaskTest {public static void main(String[] args) { Task task = new Task();new Thread(task).start();new Thread(task).start();new Thread(task).start(); }}程序运行结果:Thread-0运行,i=1Thread-2运行,i=1Thread-1运行,i=1Thread-0运行,i=2Thread-1运行,i=2Thread-2运行,i=2Thread-1运行,i=3Thread-0运行,i=3Thread-2运行,i=3
通过以上的程序可以看出,如果没有设置线程的名称,系统会为其自动创建名称,名称的格式为Thread-数字(从0开始的自增值)
通过以下jdk中Thread类的源码可以证明以上的内容。
public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); } private static int threadInitNumber; // 静态的变量,用于匿名线程的自增private static synchronized int nextThreadNum() {return threadInitNumber++; }
示例2:使用setName方法设置线程名称,并获取线程名称
// 测试类public class TaskTest {public static void main(String[] args) { Task task = new Task(); Thread thread = new Thread(task); thread.setName("自定义线程"); // 使用setName方法设置线程名称 thread.start(); }}程序运行结果:自定义线程运行,i=1自定义线程运行,i=2自定义线程运行,i=3
02、判断线程是否启动
通过Thread类中的start方法通知CPU这个线程已经准备好了,等待分配CPU资源后运行线程。
在Java中可以使用isAlive方法来判断线程是否已经启动。
示例1:判断线程是否启动
// 测试类public class TaskAliveTest {public static void main(String[] args) { Task task = new Task(); Thread thread = new Thread(task, "thread线程"); System.out.println("调用start方法前,thread.isAlive=" + thread.isAlive()); thread.start(); System.out.println("调用start方法后,thread.isAlive=" + thread.isAlive());for (int i = 0; i < 3; i++) { System.out .println(Thread.currentThread().getName() + "线程运行,i=" + i); } System.out.println("main方法执行完毕时,thread.isAlive=" + thread.isAlive()); }}程序运行结果:调用start方法前,thread.isAlive=false调用start方法后,thread.isAlive=truemain线程运行,i=0main线程运行,i=1main线程运行,i=2main方法执行完毕时,thread.isAlive=true线程运行,i=1线程运行,i=2线程运行,i=3
从上面的程序看以看出,在调用thread线程的start方法前,thread线程没有存活,调用start方法后,thread线程启动了,随后CPU资源被main线程抢占,开始执行main线程的代码,main方法执行完毕时,thread线程依然存活着,最后thread线程获得了CPU的资源,开始执行自己的run方法的代码,输出1-3的数字。
示例2:main线程加入休眠,判断线程是否存活
// 测试类public class TaskAliveTest {public static void main(String[] args) { Task task = new Task(); Thread thread = new Thread(task, "thread线程"); System.out.println("调用start方法前,thread.isAlive=" + thread.isAlive()); thread.start(); System.out.println("调用start方法后,thread.isAlive=" + thread.isAlive());try { Thread.sleep(10); // 为了观察线程代码执行后,线程是否存活,让主线程休眠 } catch (InterruptedException e) { e.printStackTrace(); }for (int i = 0; i < 3; i++) { System.out .println(Thread.currentThread().getName() + "线程运行,i=" + i); } System.out.println("main方法执行完毕时,thread.isAlive=" + thread.isAlive()); }}程序运行结果:调用start方法前,thread.isAlive=false调用start方法后,thread.isAlive=truethread线程运行,i=1thread线程运行,i=2thread线程运行,i=3main线程运行,i=0main线程运行,i=1main线程运行,i=2main方法执行完毕时,thread.isAlive=false
上面的程序和示例1的区别就是在main方法开始循环前,让main线程休眠10毫秒,目的是为了让出CPU给thread线程运行,thread线程执行完毕后,main线程开始执行循环输出1-3的数字,最后main方法执行完毕时,thread线程已经不存活了。
通过以上两个程序的演示,可以说明:
1、main方法的运行也是一个线程,也就是我们常说的主线程。
2、因为线程操作具有不确定性,所以主线程有可能最先执行完,当主线程执行完毕的时候,其他线程不会随着主线程的结束而结束,其他线程会执行完自己的操作之后才结束。(示例1可以说明这个问题)。
3、isAlive方法可以判断线程是否存活,线程存活是指线程介于启动状态和死亡状态之间的状态。
03、线程的强制运行
在线程操作中,我们可以使用join方法让一个线程强制运行,某一个线程强制运行期间,其他线程无法运行,必须等待该线程执行完成之后才可以执行。
示例:线程的强制运行
为了更好地看出演示的效果,我们把上面的Task线程类修改为输出1-10的数字。
public class Task1 implements Runnable { @Overridepublic void run() {for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + "运行,i=" + i); } }}// 测试类public class TaskJoinTest {public static void main(String[] args) { Task task = new Task(); Thread thread = new Thread(task, "thread线程"); thread.start();for (int i = 0; i < 5; i++) {if (i > 2) {try { thread.join(); //当主线程的i值大于2时,强制让thread线程执行 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + "运行,i=" + i); } }}程序运行结果:main运行,i=0thread线程运行,i=1main运行,i=1thread线程运行,i=2main运行,i=2thread线程运行,i=3thread线程运行,i=4thread线程运行,i=5thread线程运行,i=6thread线程运行,i=7thread线程运行,i=8thread线程运行,i=9thread线程运行,i=10main运行,i=3main运行,i=4
从以上的程序可以看出,在主线程的i值不大于2的时候,主线程和thread线程随机执行,当主线程的i值大于2之后,由于thread线程执行了join方法,也就是说强制让CPU分配给thread线程使用,因此,thread线程输出到10之后,直到thread线程结束运行,CPU才分配给主线程执行。
也就是说当一个线程调用了join方法后,它就会独占CPU资源,直到它完成线程的所有操作,CPU资源才会分配给其他线程执行。
04、线程的休眠
在程序中直接使用Thread.sleep()方法即可让一个线程进行休眠,这个我们很熟悉了,上面的代码中也有使用,这里就不多做介绍了。
sleep方法的作用是使当前正在执行的线程进入睡眠状态(暂时停止执行)以指定的毫秒数为准。
调用sleep方法时要注意,不能用线程的实例对象去调用sleep方法,而是要以静态的方式去调用,即直接使用Thread.sleep(要休眠的毫秒数);同时要注意捕获InterruptedException。
05、线程的礼让
在线程操作中,可以使用yield方法将一个线程的操作暂时让给其他线程执行。
public class TaskYield implements Runnable { @Overridepublic void run() {for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + "运行,i=" + i);if (i == 3) { System.out.print(Thread.currentThread().getName() + "礼让:"); Thread.yield(); // 线程礼让 } } }}// 测试类public class TaskYieldTest {public static void main(String[] args) { TaskYield task = new TaskYield(); Thread ta = new Thread(task, "线程A"); Thread tb = new Thread(task, "线程B"); ta.start(); tb.start(); }}程序运行结果:(运行结果有多种)线程A运行,i=1线程B运行,i=1线程A运行,i=2线程A运行,i=3线程B运行,i=2线程A礼让:线程B运行,i=3线程A运行,i=4线程B礼让:线程A运行,i=5线程B运行,i=4线程B运行,i=5
从程序的运行结果来看,yield方法可以使当前线程从运行状态变为就绪状态。CPU会从就绪状态里调度线程去执行,此时刚才礼让出来的线程还是有可能会被再次执行到的,不是说礼让的线程就一定不会被CPU调度到了。
也就是说yield方法可以是线程暂时让出CPU,但是也有可能继续被CPU调度而接着执行。
yield方法和sleep方法的区别:
sleep方法会导致当前线程暂停指定的时间,没有CPU时间片的消耗
yield方法会使运行状态的线程进入就绪状态,继续等待CPU的调度
06、守护线程
在Java程序中有两类线程,分别是用户线程(前台线程)、守护线程(后台线程)。
JVM处理垃圾回收的GC线程就是后台线程,我们在开发中创建的线程用于处理指定的任务的一般都是前台线程。
从前台线程转变为后台线程可以在start方法启动前使用setDaemon(true)方法来将其设置为后台线程,使用isDaemon()来判断线程是否为后台线程。
示例1:把一个线程设置为后台线程,并判断其是否为后台线程
public class DaemonThread implements Runnable { @Overridepublic void run() {while(true){ // 无限循环 System.out.println(Thread.currentThread().getName()+"运行中"); } }}// 测试类public class DaemonThreadTest {public static void main(String[] args) { DaemonThread dt = new DaemonThread(); Thread thread = new Thread(dt); thread.setDaemon(true); // 设置为后台线程 thread.start(); System.out.println("是否为后台线程:" + thread.isDaemon()); System.out.println("main线程执行结束"); }}程序运行结果:是否为后台线程:truemain线程执行结束Thread-0运行中Thread-0运行中Thread-0运行中Thread-0运行中...最终程序停止输出,并退出运行
示例2:同样的无限循环的线程,没有设置为后台线程,观看程序结果
// 测试类public class DaemonThreadTest {public static void main(String[] args) { DaemonThread dt = new DaemonThread(); Thread thread = new Thread(dt); thread.start(); System.out.println("是否为后台线程:" + thread.isDaemon()); System.out.println("main线程执行结束"); }}程序运行结果:是否为后台线程:falsemain线程执行结束Thread-0运行中Thread-0运行中Thread-0运行中Thread-0运行中...程序一直输出,不会退出运行
从上面的两个程序的演示结果可以明显看出:
1、用户线程全部执行完毕,只有后台线程的时候,那么后台线程将会终止执行。也就是说,当所有非后台线程执行完毕时,后台线程也会停止执行。main线程是非后台线程。
2、只要有一个用户线程在执行,即使主线程执行完毕,Java虚拟机也不会退出,会继续执行用户线程。
07、线程的优先级
Java所有的线程在运行前都会保持在就绪状态,哪个线程的优先级高,哪个线程就有可能会先执行。
Java程序中有最高、中等、最低3种优先级,使用setPriority方法可以设置一个线程的优先级。
示例1:演示3种不同优先级的线程执行结果
public class TaskPriority implements Runnable {@Overridepublic void run() {for (int i = 1; i <= 3; i++) {try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运行,i=" + i); } }}// 测试类public class TaskPriorityTest {public static void main(String[] args) { Thread ta = new Thread(new TaskPriority(),"线程A"); Thread tb = new Thread(new TaskPriority(),"线程B"); Thread tc = new Thread(new TaskPriority(),"线程C"); ta.setPriority(Thread.MAX_PRIORITY); //设置A线程的优先级为最高 tb.setPriority(Thread.NORM_PRIORITY); //设置B线程的优先级为中等 tc.setPriority(Thread.MIN_PRIORITY); //设置C线程的优先级为最低 ta.start(); tb.start(); tc.start(); }}程序运行结果:线程A运行,i=1线程B运行,i=1线程C运行,i=1线程A运行,i=2线程B运行,i=2线程C运行,i=2线程A运行,i=3线程B运行,i=3线程C运行,i=3再次运行结果:线程C运行,i=1线程B运行,i=1线程A运行,i=1线程B运行,i=2线程C运行,i=2线程A运行,i=2线程C运行,i=3线程B运行,i=3线程A运行,i=3
通过以上的程序运行结果可以看到,虽然线程可以设置优先级来决定哪个线程先运行,实际上哪个线程先执行是由CPU的调度决定的,并不是高优先级的线程一定会先执行。
示例2:获取主方法的优先级
使用getPriority方法可以获取一个线程的优先级。
![50e1b14a95411381ac8674439a389e5d.png](https://i-blog.csdnimg.cn/blog_migrate/422dd670611bb53edda565ea1e8125a1.jpeg)
public class GetPriorityTest {public static void main(String[] args) { System.out.println("主方法的优先级是:" + Thread.currentThread().getPriority()); }}程序运行结果:主方法的优先级是:5
以上程序说明,主方法main线程的优先级常量是5,从上面的表格中可以看出,5代表中等优先级。因此说主线程的优先级是中等级别,一般情况下,线程默认的优先级就是中等级别。
08、线程的中断
当一个线程运行时,另外一个线程可以直接通过interrupt方法对其设置中断标志位。
判断线程是否中断的2个方法:
// 判断目标线程是否被中断,不会清除中断标记。Thread.currentThread().isInterrupted()// 判断目标线程是否被中断,会清除中断标记Thread.interrupted()
示例1:判断目标线程是否被中断,不会清除中断标记
public class Task2 implements Runnable { @Override public void run() { System.out.println("线程的run方法开始执行"); // 判断线程是否被中断,不会清除中断标记 if (Thread.currentThread().isInterrupted()) { System.out.println("线程被中断"); } if (!Thread.currentThread().isInterrupted()) { // 因为上面的Thread.currentThread().isInterrupted()不会清除中断标记 // 因此线程保留了中断标记,所以该循环不会执行,程序不会输出1-3的数字 for (int i = 0; i < 3; i++) { System.out.println("线程运行,i=" + i); } } System.out.println("线程的run方法执行结束"); }}运行上面的测试类Task2InterruptTest,程序运行结果:线程的run方法开始执行线程被中断线程的run方法执行结束
以上的程序说明,Thread.currentThread().isInterrupted()不会清除中断标记,因此线程保留了中断标记,所以该循环不会执行,程序不会输出1-3的数字。
示例2:判断目标线程是否被中断,清除中断标记
public class Task3 implements Runnable { @Override public void run() { System.out.println("线程的run方法开始执行"); // 判断线程是否被中断,会清除中断标记 if (Thread.interrupted()) { System.out.println("线程被中断"); } if (!Thread.currentThread().isInterrupted()) { // 因为上面的Thread.interrupted()会清除中断标记 // 因此线程的中断标记没有了,线程继续执行,程序会输出1-3的数字 for (int i = 0; i < 3; i++) { System.out.println("线程运行,i=" + i); } } System.out.println("线程的run方法执行结束"); }}// 测试类public class Task3InterruptTest { public static void main(String[] args) { Task3 task = new Task3(); Thread thread = new Thread(task); thread.start(); thread.interrupt(); // 中断thread线程的执行 }}程序运行结果:线程的run方法开始执行线程被中断线程运行,i=0线程运行,i=1线程运行,i=2线程的run方法执行结束
以上的程序说明,Thread.interrupted()会清除中断标记,thread线程没有了中断标记,因此Thread.currentThread().isInterrupted()的结果为false,所以线程继续执行,程序会输出1-3的数字。
下面我们来看一下如果处于阻塞状态(调用了sleep、join等方法)的线程,被执行了中断操作,会有什么样的结果。
答案很明显,处于阻塞状态下的线程被执行了中断操作,会抛出中断异常InterruptedException。
示例3:处于阻塞状态的线程被执行中断操作的结果演示
public class Task4 implements Runnable { @Override public void run() { System.out.println("线程的run方法开始执行"); try { Thread.sleep(5000); // 线程休眠5秒 System.out.println("线程完成5秒钟的休眠"); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程的run方法执行结束"); }}// 测试类public class Task4InterruptTest { public static void main(String[] args) { Task4 task = new Task4(); Thread thread = new Thread(task); thread.start(); try { Thread.sleep(2000); // 主线程休眠2秒 } catch (InterruptedException e) { e.printStackTrace(); } thread.interrupt(); // 中断thread线程的执行 }}程序运行结果:线程的run方法开始执行java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at testThread.day3.Task4.run(Task4.java:8) at java.lang.Thread.run(Unknown Source)线程的run方法执行结束
通过上面的程序可以看出,处于阻塞状态下的线程被执行了中断操作,会抛出中断异常InterruptedException,但是thread线程在捕获到异常后,输出了“线程的run方法执行结束”的文字,说明thread线程在接收到中断指令后,并没有中断线程的执行,而是继续向下执行。
通过以上的程序示例,我们可以得出一个结论,那就是线程在执行了中断指令后,其实是给线程发了一个中断信号,线程被打上中断标记,如果线程没有对中断标记进行判断,做相应的处理,那么线程默认会继续执行,直到线程操作结束。
那么,问题来了,处于阻塞状态的线程被执行中断指令后,如何做到线程的中断呢,请看以下的代码。
示例4:处于阻塞状态的线程被执行中断指令后,立即把线程中断
public class Task5 implements Runnable { @Override public void run() { System.out.println("线程的run方法开始执行"); try { Thread.sleep(5000); // 线程休眠5秒 System.out.println("线程完成5秒钟的休眠"); } catch (InterruptedException e) { // 在catch块里进行处理,再次调用interrupt方法,线程是否会中断执行呢? System.out.println("线程被中断"); Thread.currentThread().interrupt(); } for (int i = 0; i < 3; i++) { System.out.println(i); } System.out.println("线程的run方法执行结束"); }}// 测试类public class Task5InterruptTest { public static void main(String[] args) { Task5 task = new Task5(); Thread thread = new Thread(task); thread.start(); try { Thread.sleep(2000); // 主线程休眠2秒 } catch (InterruptedException e) { e.printStackTrace(); } thread.interrupt(); // 中断thread线程的执行 }}程序运行结果线程的run方法开始执行线程被中断012线程的run方法执行结束
通过上面的程序,大家会感到奇怪,明明已经在catch块里,又调用了Thread.currentThread().interrupt()方法,但是线程还是没有中断,继续往下执行。
在这里再一次和大家要强调说明的就是,调用线程的interrupt()方法,不是说线程就不执行了,而是向线程发出了中断信号,线程被打上中断标记,如果想让线程中断,必须在run方法里对中断信号进行响应,让程序return返回到被调用处,才能使线程真正的中断。
示例5:正确的让线程中断的例子
public class Task6 implements Runnable { @Override public void run() { System.out.println("线程的run方法开始执行"); try { Thread.sleep(5000); // 线程休眠5秒 System.out.println("线程完成5秒钟的休眠"); } catch (InterruptedException e) { // 在catch块里进行处理,让程序返回被调用处 System.out.println("线程被中断"); return; } for (int i = 0; i < 3; i++) { System.out.println(i); } System.out.println("线程的run方法执行结束"); }}// 测试类public class Task6InterruptTest { public static void main(String[] args) { Task6 task = new Task6(); Thread thread = new Thread(task); thread.start(); try { Thread.sleep(2000); // 主线程休眠2秒 } catch (InterruptedException e) { e.printStackTrace(); } thread.interrupt(); // 中断thread线程的执行 }}程序运行结果:线程的run方法开始执行线程被中断
通过上面的程序,可以看出,使用return可以做到中断线程。
线程的中断其实是为了优雅的停止线程的运行,为了不使用stop方法而设置的。因为JDK不推荐使用stop方法进行线程的停止,因为stop方法会释放锁并强制终止线程,会造成执行一半的线程终止,带来数据的不一致性。
以上内容对多线程的生命周期以及Thread类的八个常用线程方法进行了说明,希望大家可以熟练掌握。
为了感谢支持我的朋友!整理了一份Java高级架构资料、Spring源码分析、Dubbo、Redis、Netty、zookeeper、Spring cloud
关注我的头条号并在后台私信我:555,(即可获取)。
不知道怎么私信的朋友可以关注公众号:Java耕耘者。(点击小助理获取)