1。开篇词 说到并发编程,可能大家脑海中的第一印象会是Thread、多线程、JUC、线程池、ThreadLocal等等内容。确实,并发编程是Java编程中不可或缺的一部分,掌握并发编程的核心技术,在求职面试中会是摧城拔寨的利器。而今天将要跟大家一起聊聊的是:并发编程的基石Thread类的工作原理。 事实上,在笔者回忆关于Thread类的核心API以及对应的线程状态转换关系时,总觉得印象有一些模糊,故此才有这篇文章。本文的核心议题是Thread类,由此延伸出诸多议题,例如:进程与线程、线程状态及生命周期、ThreadAPI的用法等等。2。进程与线程 首先,有必要介绍一下进程与线程,以及它们之间的区别与关系。 进程是操作系统分配资源的基本单位,比如我们在启动一个main此时就启动了一个JVM进程。 而线程则是比进程纬度更小的单位,它是CPU分配的基本单位(因为真正占用运行的就是线程),比如启动一个main方法后它所在的线程就属于这个JVM进程的一个线程,它的名字叫主线程。一个进程可以有一个或多个线程,同一个进程中的各个线程之间共享进程的内存空间。 进程与线程之间的区别如下:进程是操作系统分配资源的最小单位,而线程是CPU分配(程序执行)的最小单位一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间调度和切换:线程上下文切换比进程上下文切换要快得多3。Thread常用API 鉴于笔者此前对Thread类核心API知之甚少,对其原理不甚了解,于是本小节主要内容就是介绍Thread类核心API的用法、意义及对线程状态的影响等内容。3。1创建线程任务 在调用Thread类的API之前,需要先创建线程对象,这很简单,只需要newThread()就可以了。但是事实上,如果只通过new创建线程并未做其他任何操作,那么这个线程将不会执行任何业务逻辑。 所以我们需要通过另外的手段为线程指定其执行的业务逻辑,那么问题来了:创建线程任务有几种形式? 一般来说,我们认为有三种形式:继承Thread类、实现Runnable接口以及实现Callable接口,下面一一进行详述。3。1。1继承Thread类 创建一个类,让它继承Thread类并覆盖run方法,run方法中指定了线程执行的业务逻辑。这样在以后创建线程时可以直接实例化该类即可,在启动线程后程序会自动去执行覆盖的run方法逻辑。publicclassCreateThreadByThreadextendsThread{Overridepublicvoidrun(){System。out。println(CreateThreadByRunnablerun,自定义的业务逻辑);}publicstaticvoidmain(String〔〕args){ThreadthreadnewCreateThreadByThread();thread。start();}}输出CreateThreadByRunnablerun,自定义的业务逻辑3。1。2实现Runnable接口 创建一个类,实现Runnable接口并覆盖run方法是,然后再去创建一个Thread类,并将实现Runnable接口的对象作为入口传入Thread类的构造器,在启动Thread对象后程序会去执行Runnable对象的run方法。publicclassCreateThreadByRunnableimplementsRunnable{Overridepublicvoidrun(){System。out。println(CreateThreadByRunnablerun,自定义的业务逻辑);}publicstaticvoidmain(String〔〕args){RunnablerunnablenewCreateThreadByRunnable();将runnable对象作为入参传入Thread类构造器ThreadthreadnewThread(runnable);thread。start();}} 虽然以上两种形式一个是继承Thread类,一个是实现Runnable接口,但如果细看源码的话并没有本质上的区别。 首先看继承Thread类似的形式,它需要覆盖run方法,我们来看看Threadrun默认内容是什么:ThreadrunOverridepublicvoidrun(){if(target!null){target。run();}}ThreadtargetWhatwillberun。privateRunnabletarget; 事实上,Threadrun方法是覆盖了Runnable接口的run方法,它的逻辑就是当Runnable类型私有成员变量不为空时,执行其run方法。 而实现Runnable接口的形式,我们在创建完Runnable类型的对象后,需要将它作为入参传入Thread类的构造器。ThreadThread(java。lang。Runnable)publicThread(Runnabletarget){init(null,target,ThreadnextThreadNum(),0);}privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,longstackSize,AccessControlContextacc,booleaninheritThreadLocals){省略其他代码this。targettarget;省略其他代码} 可以看到在Thread类似的重载构造方法中,传入的Runnable类型的对象赋值给了Thread类的target私有成员变量。再联系我们刚刚提到的Threadrun方法:当Runnable类型私有成员变量不为空时,执行其run方法。 这不就是换了个皮吗? 所以实现Runnable接口的形式跟继承Thread类的形式并没有本质上的区别,它们都是基于覆盖run方法来实现改变线程需要执行的任务。3。1。3实现Callable接口 Callable接口是JDK1。5才引入的类,它的功能比Runnable更强大,最大的特点是Callable允许有返回值,其次它支持泛型,同时它允许抛出异常被外层代码捕获,下面是实现Callable接口来创建线程的示例:publicclassCreateThreadByCallableimplementsCallableInteger{publicstaticvoidmain(String〔〕args){CreateThreadByCallablecallablenewCreateThreadByCallable();FutureTaskIntegerfuturenewFutureTask(callable);创建线程并启动ThreadthreadnewThread(future);thread。start();Integerintegernull;try{integerfuture。get();}catch(InterruptedExceptionExecutionExceptione){e。printStackTrace();}System。out。println(FutureTask返回内容:integer);}OverridepublicIntegercall()throwsException{System。out。println(CreateThreadByCallablecall,自定义的业务逻辑,返回1);return1;}} 值得注意的是,我们需要基于FutureTask类配合使用Callale接口,返回值、异常以及泛型都是FutureTask类提供的特性。 当深入FutureTask的构造器以及其内部方法时,笔者发现了一些新东西。FutureTaskFutureTask(java。util。concurrent。CallableV)publicFutureTask(CallableVcallable){if(callablenull)thrownewNullPointerException();this。callablecallable;this。stateNEW;} 首先在FutureTask的构造器中,将Callable对象赋值给了FutureTask类的Callable类型私有成员变量。然后继续构造Thread对象,笔者发现咱们使用的Thread重载构造方法竟然与实现Runnable接口的场景是一致的,也就是说FutureTask实现了Runnable接口,打开源码一看,果然如此。FutureTask类实现了RunnableFuture接口publicclassFutureTaskVimplementsRunnableFutureV{}RunnableFuture接口继承于Runnable接口publicinterfaceRunnableFutureVextendsRunnable,FutureV{} 所以我们构造的FutureTask对象是作为Runnable类型传入Thread类中的,在线程启动时会去执行FutureTask内部的run方法,我们再来看看FutureTaskrun方法。java。util。concurrent。FutureTaskrunpublicvoidrun(){if(state!NEW!UNSAFE。compareAndSwapObject(this,runnerOffset,null,Thread。currentThread()))return;try{CallableVccallable;if(c!nullstateNEW){Vresult;booleanran;try{执行callable属性的call方法获取返回值resultc。call();rantrue;}catch(Throwableex){resultnull;ranfalse;setException(ex);}if(ran)若执行完毕,将返回值赋值给outcome属性(在FutureTaskget方法中返回)set(result);}}finally{runnernull;intsstate;if(sINTERRUPTING)handlePossibleCancellationInterrupt(s);}}java。util。concurrent。FutureTasksetprotectedvoidset(Vv){if(UNSAFE。compareAndSwapInt(this,stateOffset,NEW,COMPLETING)){outcomev;UNSAFE。putOrderedInt(this,stateOffset,NORMAL);finalstatefinishCompletion();}} 可以看到,在FutureTaskrun方法中实际上执行的是Callablecall方法。所以说,实现Callable接口的形式,最终Thread执行的内容还是Threadrun方法,只不过这个run方法是被FutureTask类的run方法覆盖了,而调用Callablecall方法是FutureTaskrun的内部预定义逻辑。3。1。4创建线程的方法 通过以上三种创建线程任务的形式以及对它们源码的探究,我们可以知道,无论是哪种形式最终还是以覆盖Threadrun的形式来实现的。 这里有一个题外话:创建线程的方法有几种? 这个问题在网络上经常被误解读,大部分观点都认为可以通过继承Thread类、实现Runnable接口以及实现Callable接口来创建线程。事实上我在上文中描述的是创建线程任务的形式,想要突出的并非创建线程的方法而是创建线程执行任务的方法。 在上文的示例代码中我们可以知道,即便是通过后两种形式(即实现Runnable接口、实现Callable接口),我们最终还是需要newThread来创建一个线程,只不过是通过传入Runnable对象来改变了线程的行为。 所以说,创建线程的方法只有一种:newThread()。3。2start Threadstart可以说是Thread类最常用的方法了,这个方法的作用是让线程开始执行。 调用刚创建好的线程的start方法后,该线程将会从NEW状态转化成RUNNABLE状态,CPU会在合适的时间分配给该线程时间片,真正执行线程的业务方法。 需要注意的是,一个线程只能调用一次start方法,否则就会抛出IllegalThreadStateException异常,原因是在Threadstart方法中,首先会判断线程状态。java。lang。Threadstartpublicsynchronizedvoidstart(){若线程状态不为NEW,抛出异常AzerostatusvaluecorrespondstostateNEW。(0值对应的状态是NEW)if(threadStatus!0)thrownewIllegalThreadStateException();省略其他逻辑。。。} 可以看到,当线程的状态不是NEW状态时,再次调用它的start方法,将会抛出IllegalThreadStateException异常。也就是说,线程一旦完成执行,就不能重新启动了。3。3join Threadjoin方法的作用是等待线程执行完毕,在JDK1。8中该方法有三个重载方法,另外两个带参数的重载方法是设置了超时时间: Threadjoin方法的意义可以这样描述:在A线程内调用B线程的join方法,A线程会等待B线程执行完毕再执行。在这个过程中,线程A的状态将由RUNNABLE转变为WAITING,B线程执行完毕后,A线程状态将转变为RUNNABLE,该结论可以通过下面的示例来验证:publicstaticvoidmain(String〔〕args)throwsInterruptedException{Threadthread1newThread((){System。out。println(thread1isrunning);try{为观察效果明显,将睡眠时间设置的长一点Thread。sleep(50000);}catch(InterruptedExceptione){e。printStackTrace();}System。out。println(thread1isover);});thread1。start();thread1。join();} 运行该程序,首先将输出thread1isrunning,然后thread1线程进入sleep方法,sleep结束后输出thread1isover。而在thread1线程sleep的过程中,打开jconsole工具,可以观察到调用了thread1。join方法的main线程状态是WAITING。 上面说的是Threadjoin方法,如果是设置了超时时间的重载方法,调用某个线程对象join方法的线程状态将转变为TIMEDWAITING。 除此之外,还有一个问题值得思考:在当前线程调用其他线程的join方法后,若其他线程尝试获取当前线程已持有的锁,是否会成功?我们来做个实验。privatestaticStringstr123;privatestaticvoidtestJoinLock()throwsInterruptedException{main线程先占用str资源synchronized(str){Threadthread1newThread((){System。out。println(thread1isrunning);子线程尝试占用str资源synchronized(str){System。out。println(thread1isgetstrlock);}System。out。println(thread1isover);});thread1。start();thread1。join();}} 先声明一个共享资源str变量,main线程首先对该变量加上同步锁,然后实例化一个子线程,子线程中也尝试去给str加上同步锁。运行该程序,观察到最终输出的内容是:thread1isrunning,且程序一直未终止。 猜测可能子线程时被阻塞了,打开jconsole,如下图: 果然,观察到Thread0线程的状态是BLOCKED,且资源拥有者是main,即该线程被main线程阻塞了。 所以,当线程A因调用线程B线程的join方法而进入WAITING状态时,并不会释放本身已持有的锁资源。3。4yield Threadyield方法的作用是释放时间片,让CPU再次选择线程执行。这句话潜在的意思是说CPU可能选中之前放弃时间片的线程来执行。 值得注意的是,Threadyield方法不会释放已经持有的锁资源。3。5interrupt Threadinterrupt方法的作用是中断线程,调用线程的该方法会请求终止当前线程,需要注意的是该方法仅仅是给当前线程发送了一个终止的信息,并设置中断标志位,最终是否终止是线程自己处理的。 还有两个比较类似的方法:Threadinterrupted,ThreadisInterrupted。 Threadinterrupt方法的作用是检查该线程是否被中断,同时清除中断标志位。 ThreadisInterrupted方法的作用是检查该线程是否被中断,但不清除中断标志位。 值得注意的是,当调用线程的Threadinterrupt方法时,若当前线程处于TIMEDWAITING或WAITING状态时(如调用过Objectwait,Threadjoin,Threadsleep或对应重载方法的线程),将会抛出InterruptedException异常,使得线程直接进入TERMINATED状态。publicstaticvoidmain(String〔〕args){Threadthread1newThread((){System。out。println(thread1isrunning);try{为观察效果明显,将睡眠时间设置的长一点Thread。sleep(50000);}catch(InterruptedExceptione){e。printStackTrace();}System。out。println(thread1isover);});thread1。start();中断线程thread1。interrupt();} 执行该方法,输出内容为:thread1isrunningthread1isoverjava。lang。InterruptedException:sleepinterruptedatjava。lang。Thread。sleep(NativeMethod)atio。walkers。planes。pandora。jdk。thread。usage。InterruptMethod。lambdamain0(InterruptMethod。java:16)atjava。lang。Thread。run(Thread。java:748) 可以看到,程序抛出了InterruptedException异常。4。线程状态 在Java语言中,线程被抽象成Thread类,而在Thread类中有一个State枚举类,它描述了线程的各种状态。java。lang。Thread。StatepublicenumState{NEW,RUNNABLE,BLOCKED,WAITING,TIMEDWAITING,TERMINATED;} JDK源码的注释对于线程状态的描述如下:NEW:未启动的线程处于这种状态RUNNABLE:在JVM中执行的线程处于这种状态BLOCKED:等待锁而被阻塞的线程处于这种状态WAITING:一个线程正在无限期地等待另一个线程执行某个特定的操作,它就处于这种状态TIMEDWAITING:在指定的等待时间内等待另一个线程执行某个操作的线程处于这种状态TERMINATED:已退出的线程处于这种状态 下面我就用代码模拟处于各个状态的线程,同时会例举线程进入该状态的方法。4。1NEW 未启动的线程处于NEW状态,这个状态十分容易模拟,当我new出一个Thread对象后,该Thread线程就处于NEW状态,模拟代码如下:publicstaticvoidmain(String〔〕args){ThreadthreadnewThread();System。out。println(Threadstateis:thread。getState());}程序输出内容Threadstateis:NEW 所以创建一个线程对象后,该线程的状态就是NEW状态。4。2RUNNABLE 在JVM中执行的线程处于RUNNABLE状态,即当调用一个线程的start方法后,等待CPU分配给该线程时间片,该线程正式执行时,他就处于RUNNABLE状态,模拟代码如下:publicstaticvoidmain(String〔〕args){ThreadthreadnewThread(()System。out。println(Threadstateis:Thread。currentThread()。getState()));thread。start();}程序输出内容Threadstateis:RUNNABLE 所以调用一个线程的start方法后,若未抛出异常,该线程就会进入RUNNABLE状态。 这里需要注意的是,若对于非NEW状态的线程调用它的start方法,将会抛出IllegalThreadStateException异常,原因源码如下:java。lang。Threadstartpublicsynchronizedvoidstart(){若线程状态不为NEW,抛出异常AzerostatusvaluecorrespondstostateNEW。(0值对应的状态是NEW)if(threadStatus!0)thrownewIllegalThreadStateException();省略其他逻辑。。。} 除此之外,还有几种情况也会进入RUNNABLE状态:BLOCKED状态下的线程因获取锁成功而进入RUNNABLE状态因调用sleep,join方法而进入WAITINGTIMEDWAITING状态的线程,超过超时时间、正常等待结束或调用Objectnotify,ObjectnotifyAll方法,会进入RUNNABLE状态RUNNABLE状态的线程因调用yield方法而重新进入RUNNABLE状态4。3BLOCKED 线程由于等待锁而被阻塞将处于BLOCKED状态,想要模拟该状态下的线程就需要引入共享资源以及第二个线程了,线程1先启动并占有锁资源,然后再启动线程2,当线程2尝试获取锁资源时,发现共享资源已被线程1占用,于是进入阻塞状态。模拟代码如下:publicclassStateBlocked{共享资源privatestaticStringstrlock;publicstaticvoidmain(String〔〕args)throwsInterruptedException{Threadthread1newThread((){synchronized(str){System。out。println(Thread1getlock);防止线程thread1释放str锁资源try{Thread。sleep(10000);}catch(InterruptedExceptione){e。printStackTrace();}}});thread1。start();保证thread1先拿到锁资源Thread。sleep(1000);Threadthread2newThread((){synchronized(str){System。out。println(Thread1getlock);}});thread2。start();保证thread2进入synchronized代码块Thread。sleep(1000);System。out。println(Thread2stateis:thread2。getState());}} 上述模拟代码的输出结果如下:Thread1getlockThread2stateis:BLOCKEDThread1getlock 所以线程由于进入同步块尝试获取锁失败被阻塞时,其状态就是BLOCKED状态。4。4WAITING 一个线程正在无限期地等待另一个线程执行某个特定的操作,它就处于WAITING状态。启动一个线程A,在另一个线程B中调用Threadjoin方法,线程B会等待线程A执行完毕,这时线程B就是WAITING状态,模拟代码如下:publicstaticvoidmain(String〔〕args)throwsInterruptedException{锁资源Stringstrlock;ThreadthreadnewThread((){sleep100s是为了有足够的时间查看线程状态try{Thread。sleep(100000);}catch(InterruptedExceptione){e。printStackTrace();}});thread。start();主线程等待thread线程执行完毕thread。join();} 执行该方法后,我们需要打开jconsole,找到对应进程,查看其线程状态,如下图: 可以看到,此时main线程的状态是WAITING。 所以,调用一个线程的join方法(不指定超时时间),调用方将进入WAITING状态。 除此之外,调用一个线程的Objectwait(不指定超时时间),调用方也将进入WAITING状态。4。5TIMEDWAITING 在指定的等待时间内等待另一个线程执行某个操作的线程处于TIMEDWAITING状态。TIMEDWAITING状态与WAITING状态唯一的不同就是前者指定了超时时间,在上一步代码基础上略作改动,我们就可以模拟出TIMEDWAITING状态,模拟代码如下:publicstaticvoidmain(String〔〕args)throwsInterruptedException{锁资源Stringstrlock;ThreadthreadnewThread((){sleep100s是为了有足够的时间查看线程状态try{Thread。sleep(100000);}catch(InterruptedExceptione){e。printStackTrace();}});thread。start();主线程等待thread线程执行完毕,指定超时时间为10sthread。join(10000);} 执行该方法后,打开jconsole,找到对应进程,查看其线程状态,如下图: 可以看到,此时main线程的状态是TIMEDWAITING。 所以,调用一个线程的join方法(指定超时时间),调用方将进入TIMEDWAITING状态 除此之外,调用一个线程的Objectwait(指定超时时间),调用方也将进入WAITING状态。4。5TERMINATED 已退出的线程处于TERMINATED状态,这个状态就是线程自然结束的状态,十分容易模拟,代码如下:publicstaticvoidmain(String〔〕args)throwsInterruptedException{ThreadthreadnewThread();thread。start();等待线程thread执行完毕thread。join();System。out。println(Threadstateis:thread。getState());} 所以,当一个线程正常结束时,它将进入TERMINATED状态。 除此之外,当一个线程抛异常退出时,也会进入TERMINATED状态,例如在WAITINGTIMEDWAITING状态下的线程调用Threadinterrupt方法而退出。5。线程状态转换图 将上述的线程状态转换关系总结为如下图: 6。小结 本文首先介绍了进程与线程的联系与区别,随后描述了Thread类常用的API以及创建线程执行任务的形式,然后详细说明了线程的状态并作出示例,最后总结了线程状态转换图。 总结了几个小问题:进程与线程的联系与区别创建线程任务的形式有哪几种?创建线程有几种方式?线程状态有哪几种?描述一下线程状态的转换规则