)
Java 线程核心 API、线程分类、终止方式与六大生命周期状态前言Thread线程七大属性守护线程 vs 用户线程核心规则实操DemosetDaemon(true)案例启动线程终止线程自定义布尔标记位使用Thread.interrupt()代替自定义标志位线程等待获取当前线程的引用休眠当前线程线程的状态全文总结前言承接上一篇进程与线程、五种线程创建方式本篇深入讲解 Java 线程核心 API线程七大属性、守护线程、两种安全终止方案、join 等待方法、线程六大生命周期状态全部配套实操代码吃透 Java 线程底层控制逻辑。Thread线程七大属性IDgetId()线程的身份标识标识一个进程中唯一的一个线程。是JVM分配的不是系统API提供的或PCB中的ID。名称getName()、setName()自定义线程名方便日志排查状态getState()获取当前线程生命周期状态优先级getPriority()调度参考不绝对控制执行顺序是否后台线程守护线程isDaemon()、setDaemon()区分后台线程还是用户线程是否存活isAlive()判断内核线程是否存活Thread对象的生命周期要比系统内核中的线程更长回调方法执行完毕内核线程就没了。是否被中断isInterrupted()中断标记位判断守护线程 vs 用户线程核心规则用户线程前台一个Java进程中若前台线程没有执行结束此时整个进程都不会结束JVM必须等待全部用户线程执行完毕才会退出。守护线程后台仅作为服务支撑所有用户线程结束之后JVM强制销毁全部守护线程不等其执行完毕因此守护线程不结束不影响整个进程的结束。默认所有线程都是用户线程GC垃圾回收线程是内置守护线程。除了GC垃圾回收线程剩下所有的线程必须通过手动设置才是守护线程。实操DemosetDaemon(true)案例publicclassDemo{publicstaticvoidmain(String[]args){// TODO 自动生成的方法存根ThreadthreadnewThread(){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{privatestaticbooleanisQuitfalse;publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadthreadnewThread(()-{while(!isQuit){System.out.println(线程工作中);try{Thread.sleep(1000);}catch(InterruptedExceptione){// TODO 自动生成的 catch 块e.printStackTrace();}}System.out.println(线程工作完毕);});thread.start();Thread.sleep(3000);isQuittrue;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{ThreadthreadnewThread(()-{//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{ThreadthreadnewThread(()-{for(inti0;i5;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){ThreadthreadThread.currentThread();System.out.println(thread.getName());}}这个返回结果为main休眠当前线程核心方法Thread.sleep(ms)sleep(1000)到1000之后系统就会唤醒这个线程从阻塞态 - 就绪态这中间有一个调度的开销。因此一些对时间精确度要求很高的场景就要使用“实时操作系统”任务调度的开销在一定时间范围之内。线程的状态NEWThread对象已经有了start()方法还没有调用。TERMINATEDThread对象还在但内核中的线程已经没了。RUNNABLE就绪状态。TIMED_WAITING阻塞状态由于sleep这种固定时间的方式产生的阻塞。WAITING阻塞状态由于wait这种不固定时间的方式产生的阻塞。BLOCKED阻塞状态由于锁竞争导致的阻塞。后续定位“线程卡死”的原因时很容易就可以通过状态来初步确定了。publicclassDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadthreadnewThread(()-{});//在调用start之前获取状态此时就是NEW状态System.out.println(thread.getState());thread.start();thread.join();//当join执行结束之后线程t一定执行完毕了System.out.println(thread.getState());//此时就是TERMINATED状态}}运行结果为NEWTERMINATEDpublicclassDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadthreadnewThread(()-{while(true){//正常运行}});thread.start();for(inti0;i5;i){System.out.println(thread.getState());Thread.sleep(1000);}thread.join();}}运行结果为RUNNABLERUNNABLERUNNABLERUNNABLERUNNABLEpublicclassDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadthreadnewThread(()-{while(true){try{Thread.sleep(1000);//一旦sleep开始执行获取的状态就是TIMED_WAITING}catch(InterruptedExceptione){// TODO 自动生成的 catch 块e.printStackTrace();}}});thread.start();for(inti0;i5;i){System.out.println(thread.getState());Thread.sleep(1000);}thread.join();}}运行结果RUNNABLETIMED_WAITINGTIMED_WAITINGTIMED_WAITINGTIMED_WAITING全文总结线程有 7 个核心属性最常用ID、名称、状态、是否守护线程。用户线程不结束进程不结束守护线程不影响进程退出。start() 创建线程run() 只是任务方法。终止线程推荐使用 interrupt()而不是自定义标志位。join() 让当前线程等待目标线程执行完毕。Java 线程有 6 种状态学会状态可以排查线程卡死问题。