Java 线程核心 API 全解|守护线程、终止、join 与六大状态(面试必看)
Java 线程核心 API、线程分类、终止方式与六大生命周期状态
- 前言
- Thread线程七大属性
- 守护线程 vs 用户线程
- 核心规则
- 实操Demo:setDaemon(true)案例
- 启动线程
- 终止线程
- 自定义布尔标记位
- 使用Thread.interrupt()代替自定义标志位
- 线程等待
- 获取当前线程的引用
- 休眠当前线程
- 线程的状态
- 全文总结
前言
承接上一篇进程与线程、五种线程创建方式,本篇深入讲解 Java 线程核心 API:线程七大属性、守护线程、两种安全终止方案、join 等待方法、线程六大生命周期状态,全部配套实操代码,吃透 Java 线程底层控制逻辑。
Thread线程七大属性
- ID:getId()
线程的身份标识,标识一个进程中唯一的一个线程。是JVM分配的,不是系统API提供的或PCB中的ID。- 名称:getName()、setName()
自定义线程名方便日志排查- 状态:getState()
获取当前线程生命周期状态- 优先级:getPriority()
调度参考,不绝对控制执行顺序- 是否后台线程(守护线程):isDaemon()、setDaemon()
区分后台线程还是用户线程- 是否存活:isAlive()
判断内核线程是否存活,Thread对象的生命周期要比系统内核中的线程更长(回调方法执行完毕,内核线程就没了)。- 是否被中断:isInterrupted()
中断标记位判断
守护线程 vs 用户线程
核心规则
用户线程(前台):一个Java进程中,若前台线程没有执行结束,此时整个进程都不会结束,JVM必须等待全部用户线程执行完毕才会退出。
守护线程(后台):仅作为服务支撑,所有用户线程结束之后,JVM强制销毁全部守护线程,不等其执行完毕,因此守护线程不结束,不影响整个进程的结束。
默认所有线程都是用户线程;GC垃圾回收线程是内置守护线程。
除了GC垃圾回收线程,剩下所有的线程,必须通过手动设置才是守护线程。
实操Demo:setDaemon(true)案例
publicclassDemo{publicstaticvoidmain(String[]args){// TODO 自动生成的方法存根Threadthread=newThread(){@Overridepublicvoidrun(){// TODO 自动生成的方法存根while(true){System.out.println("hello thread");}}};//设置thread为守护线程thread.setDaemon(true);thread.start();System.out.println("OK");}}如果不写 thread.setDaemon(true);,程序会永远停不下来,一直打印 hello thread。
加上这行后,主线程打印完 OK 结束,子线程会立刻跟着结束,所以只会打印OK,偶尔会打印一两个hello thread,是因为线程切换的时间差。
启动线程
线程启动 -> start()方法。
线程启动原理:start () 与 run () 深度区分
start() 方法内部,会调用到底层 native 系统API,在系统内核中创建线程,自动回调run()。
run() 方法,就只是单纯地描述了该线程要执行什么任务(会在start方法创建好线程之后自动被调用)。
面试题
start() 与 run() 区别?
看起来效果是相似的,但本质差别在于是否在系统内部创建出新的线程。
终止线程
终止线程(打断,interrupt),就是让一个线程停止运行(销毁),Java中要想销毁/终止线程,唯一的做法就是让run()方法尽快执行结束,让它自然终止。
自定义布尔标记位
在代码中手动创建出一个标志位,作为run()方法执行结束条件
//线程的打断publicclassDemo{privatestaticbooleanisQuit=false;publicstaticvoidmain(String[]args)throwsInterruptedException{Threadthread=newThread(()->{while(!isQuit){System.out.println("线程工作中");try{Thread.sleep(1000);}catch(InterruptedExceptione){// TODO 自动生成的 catch 块e.printStackTrace();}}System.out.println("线程工作完毕");});thread.start();Thread.sleep(3000);isQuit=true;System.out.println("设置isQuit为true");}}当前的isQuit是成员变量作为标志位,如果把isQuit改成main方法中的局部变量可以吗?
不可以。lambda表达式有一个语法规则,变量捕获。lambda表达式里面的代码可以自动捕获到上层作用域中所涉及到的局部变量,但该变量不能修改。
所谓的变量捕获,其实就是让lambda表达式把当前作用域中的变量在lambda内部复制了一份。
当isQuit为成员变量时,此时lambda访问这个成员,就不是变量捕获的语法,而是“内部类访问外部类属性”,此时就没有final之类的限制了。
局部变量+lambda:只能读,不能改
成员变量+lambda:可读可写
但是上述方案需要自己手动创建变量,当线程内部sleep时,主线程修改变量,新线程内部不能及时响应,因此需要用另外的方式来完成上述线程终端的操作。
使用Thread.interrupt()代替自定义标志位
Thread类内部,有一个现成的标志位,可以用来判定当前的循环是否要结束—isInterrupted()
//线程终止publicclassDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadthread=newThread(()->{//Thread.currentThread()---获取到当前对象的实例(Thread),哪个线程调用这个方法,就会返回哪个线程的对象while(!Thread.currentThread().isInterrupted()){System.out.println("线程工作中");try{Thread.sleep(1000);}catch(InterruptedExceptione){// TODO 自动生成的 catch 块e.printStackTrace();//注意:interrupt唤醒线程之后,此时sleep方法抛出异常,同时会自动清除刚才设置的标志位->使得设置标志位这样的效果好像没有生效//Java是期望当线程收到“要中断”信号时,能够自由决定接下来要怎么处理//加一个break,让线程立即结束,也可以在break前面加一些其他工作的代码//这里操作的前提是通过“异常”的方式唤醒break;}}System.out.println("线程工作完毕");});thread.start();try{Thread.sleep(5000);}catch(InterruptedExceptione){// TODO 自动生成的 catch 块e.printStackTrace();}System.out.println("让t线程终止");thread.interrupt();//这个操作就是把上述Thread对象内部的标志位设置成true,即使线程内部的逻辑出现阻塞,也可以用这个方法唤醒//正常来说,sleep会休眠到时间到,才被唤醒,此处的interrupt就可以使sleep内部触发一个异常,从而提前被唤醒//而自己手动定义的标志位无法实现该效果}}
- interrupt() : 中断对象关联的线程。若线程正在阻塞,则以异常方式通知,否则设置标志位。
- 注意:interrupt() 先设置标志位 -> 唤醒sleep -> 抛出异常 -> 标志位被自动清除
线程等待
让一个线程等待另一个线程执行结束,再继续执行。本质上就是控制线程结束的顺序。
- 核心方法是join()—实现线程等待效果。
- 在哪个线程中调用join(),哪个线程就等待。
- 哪个对象调用join(),哪个对象被等待。
注意:如果在主线程中调用t.join(),此时就是让主线程等待t线程结束,而不是t线程等待主线程。
//线程等待publicclassDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadthread=newThread(()->{for(inti=0;i<5;i++){System.out.println("线程工作中");try{Thread.sleep(1000);}catch(InterruptedExceptione){// TODO 自动生成的 catch 块e.printStackTrace();}}});thread.start();//让主线程来等待t线程执行结束//一旦调用join,主线程就会触发阻塞,此时t线程就可以趁机完成后续的工作//一直阻塞到t执行完毕了,join才会解除阻塞,主线程才能继续执行System.out.println("join 等待开始");thread.join();System.out.println("join 等待结束");}}运行结果为:
join 等待开始
线程工作中
线程工作中
线程工作中
线程工作中
线程工作中
join 等待结束
join() 的工作过程:
- 如果t线程正在运行中,此时调用join() 的线程就会阻塞,一直阻塞到t线程执行结束为止。
- 如果t线程已经执行结束了,此时调用join(),就直接返回来,不会涉及到阻塞。
join() 默认是“死等”,一般来说,等待操作都带有一个超时时间—join(long millis),最多等这么久,实际开发中不建议“死等”,最好带有“超时时间”。
获取当前线程的引用
哪个线程调用Thread.currentThread(),就返回哪个线程的引用。
publicclassDemo{publicstaticvoidmain(String[]args){Threadthread=Thread.currentThread();System.out.println(thread.getName());}}这个返回结果为:
main
休眠当前线程
核心方法:Thread.sleep(ms)
sleep(1000):到1000之后,系统就会唤醒这个线程(从阻塞态 -> 就绪态),这中间有一个调度的开销。
因此,一些对时间精确度要求很高的场景,就要使用“实时操作系统”,任务调度的开销在一定时间范围之内。
线程的状态
- NEW:Thread对象已经有了,start()方法还没有调用。
- TERMINATED:Thread对象还在,但内核中的线程已经没了。
- RUNNABLE:就绪状态。
- TIMED_WAITING:阻塞状态(由于sleep这种固定时间的方式产生的阻塞)。
- WAITING:阻塞状态(由于wait这种不固定时间的方式产生的阻塞)。
- BLOCKED:阻塞状态(由于锁竞争导致的阻塞)。
后续定位“线程卡死”的原因时,很容易就可以通过状态来初步确定了。
publicclassDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadthread=newThread(()->{});//在调用start之前获取状态,此时就是NEW状态System.out.println(thread.getState());thread.start();thread.join();//当join执行结束之后,线程t一定执行完毕了System.out.println(thread.getState());//此时就是TERMINATED状态}}运行结果为:
NEW
TERMINATED
publicclassDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadthread=newThread(()->{while(true){//正常运行}});thread.start();for(inti=0;i<5;i++){System.out.println(thread.getState());Thread.sleep(1000);}thread.join();}}运行结果为:
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
publicclassDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadthread=newThread(()->{while(true){try{Thread.sleep(1000);//一旦sleep开始执行,获取的状态就是TIMED_WAITING}catch(InterruptedExceptione){// TODO 自动生成的 catch 块e.printStackTrace();}}});thread.start();for(inti=0;i<5;i++){System.out.println(thread.getState());Thread.sleep(1000);}thread.join();}}运行结果:
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
全文总结
- 线程有 7 个核心属性,最常用:ID、名称、状态、是否守护线程。
- 用户线程不结束,进程不结束;守护线程不影响进程退出。
- start() 创建线程,run() 只是任务方法。
- 终止线程推荐使用 interrupt(),而不是自定义标志位。
- join() 让当前线程等待目标线程执行完毕。
- Java 线程有 6 种状态,学会状态可以排查线程卡死问题。
