Java 线程:Thread 类的基本用法,从 0 开始把线程跑起来

发布时间:2026/9/5 5:53:39
Java 线程:Thread 类的基本用法,从 0 开始把线程跑起来 写这篇是因为最近把 Thread 相关的 demo 从头到尾敲了一遍,从最简单的创建线程到怎么安全地让它停、让它等,思路终于顺了。这篇就按我自己的理解捋一遍 Thread 最常用的几件事:怎么创建、怎么让它睡、怎么把它叫醒(中断)、怎么等它干完活、怎么拿当前线程。代码我都跑了,例子就直接贴我自己的。一、怎么创建一个线程创建线程这事,核心就一句话:你告诉它要干什么(run 方法),然后调 start() 让它跑起来。Java 给的方法有好几种,本质都是在给 run 方法塞代码,只不过写法越来越省事。1. 继承 Thread,重写 run最原始的一种,定义一个类继承Thread,把要干的活写进run():classMyThreadextendsThread{Overridepublicvoidrun(){while(true){System.out.println(hello Thread);try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}}publicclassDemo1{publicstaticvoidmain(String[]args){Threadt1newMyThread();t1.start();while(true){System.out.println(hello World);Thread.sleep(1000);}}}注意这里有个坑,一开始我总犯:创建线程用的是new MyThread(),但真正开跑靠的是start()。如果直接调run(),那就只是在当前线程里顺序执行了一遍方法,根本没开新线程。注释里写得很明白——start()才是开辟一个新的线程、开启并发编程的那一步。跑起来你会看到两个while(true)在同时打印,一会儿 “hello Thread”,一会儿 “hello World”,因为main方法本身自带一个线程,两个线程在抢 CPU。这就是并发最简单的样子了。2. 实现 Runnable 接口继承 Thread 有个麻烦:Java 只有单继承,这个类要是还想继承别的东西就尴尬了。所以更推荐实现Runnable,把要干的活和线程本身拆开:classMyRunnableimplementsRunnable{Overridepublicvoidrun(){while(true){System.out.println(hello runnable);Thread.sleep(1000);}}}publicclassdemo2{publicstaticvoidmain(String[]args)throwsInterruptedException{RunnablernewMyRunnable();ThreadtnewThread(r);// 把活交给线程t.start();while(true){System.out.println(hello thread);Thread.sleep(1000);}}}思路是:Runnable只负责描述干什么,Thread负责怎么跑。这样任务可以复用,一个 Runnable 可以塞给多个线程。3. 匿名内部类上面那两种都要单独定义一个类,如果任务只跑一次,用匿名内部类省得单独建文件。有两种写法,一种是继承 Thread:ThreadtnewThread(){// 创建一个 Thread 子类,但没有名字Overridepublicvoidrun(){while(true){System.out.println(hello Thread);Thread.sleep(1000);}}};t.start();另一种是实现 Runnable 塞进 Thread 构造器:ThreadtnewThread(newRunnable(){Overridepublicvoidrun(){while(true){System.out.println(hello Thread);Thread.sleep(1000);}}});t.start();4. lambda 简化任务里只有一句 run 的话,上面的匿名内部类还可以再简。Runnable是个函数式接口(只有一个抽象方法),所以能用 lambda:Threadt1newThread(()-{while(true){System.out.println(hello Thread);Thread.sleep(1000);}},线程1);// 第二个参数给线程起个名字t1.start();顺带一提,构造器里能传第二个参数作为线程名,后面排查问题看日志能分清是哪个线程在跑,这个习惯值得养成。这几种写法我日常用得最多的其实是 lambda,一行一个任务特别干净。方法之间不是谁对谁错,看场景——想复用就实现 Runnable,想简单就直接 lambda。额外一提:后台线程(守护线程)默认创建的线程都是前台线程,只要有一个前台线程活着,进程就不会结束。如果希望某个线程跟着主线程一起退出,可以设成后台线程:Threadth1newThread(()-{inti0;while(i5){System.out.println(hello Thread);i;Thread.sleep(1000);}});th1.setDaemon(true);// 设为后台线程th1.start();Thread.sleep(1000);System.out.println(主进程结束);这里主线程睡 1 秒就结束了,但新线程本来要跑 5 秒。因为它是守护线程,主线程一结束,它跟着就被干掉了,所以不会等满 5 秒。守护线程就是这种别人活着我干活,别人走了我也走的角色。二、线程休眠:sleep上面几乎每个例子都在用Thread.sleep,它就是让当前线程停下来睡一会儿再继续,单位是毫秒。Thread.sleep(1000);// 睡 1 秒Thread.sleep(10_000);// 睡 10 秒,数字里可以加下划线,好读两个容易忽略的点:一是它有个伴生问题——sleep会抛出InterruptedException,所以要么throws抛出去,要么try-catch包起来,这也是为啥前面代码里总能看到 try/catch 围着它转。二是sleep是让当前正在执行的线程睡,不是让别的线程睡。哪个线程执行到这行,哪个线程睡。所以它在主线程里,主线程睡;在新线程里,新线程睡。有时候还见过Thread.sleep(0),这不算真睡,而是主动把 CPU 让出来一下,让别的线程有机会被调度,可以用来观察多线程的抢占:RunnabletasknewRunnable(){Overridepublicvoidrun(){for(inti1;i5;i){System.out.println(Thread.currentThread().getName() 执行第 i 次循环);Thread.sleep(0);// 主动让出 CPU}}};Threadt1newThread(task,线程-A);Threadt2newThread(task,线程-B);t1.start();t2.start();同一个 Runnable 塞给两个线程,两个线程就各跑各的 5 次,sleep(0)让它们来回交替,你就能看到 A、B 交叉着打印,而不是 A 一口气跑完再轮到 B。三、线程中断线程一旦跑起来,while(true)那样的循环基本停不下来。怎么让线程停下来?以前有stop()这种强制方法,太暴力,现在已经不建议用了。正规的做法是给线程发一个中断信号,让线程自己判断要不要停。1. 用标记变量自己控制最简单直白的思路——准备一个布尔变量当开关,线程循环里检查它,外部改掉它线程就停:publicstaticbooleanisTuretrue;publicstaticvoidmain(String[]args){ThreadtnewThread(()-{while(isTure){System.out.println(hello thread);Thread.sleep(1000);}System.out.println(终止);});t.start();ScannerscannernewScanner(System.in);System.out.println(如果你输入0,那么进程将会终止);intnscanner.nextInt();if(n0){isTurefalse;// 改标记,线程下次循环就退出}}逻辑很顺:主线程从键盘读个数字,输 0 就把isTure改成 false,子线程正在while(isTure)循环里,发现条件不满足了,跳出循环,打印终止。不过这个写法在个别情况下有个隐患——线程在循环里还 sleep,或者编译器做了优化,标记变量可能不会立刻可见,后面会引出 volatile 的话题。这边先记着有这么回事。2. 用 interrupt 发中断信号Java 官方更推荐的思路是interrupt()。它不是直接把线程杀死,而是往线程身上挂一个中断标记,线程自己可以通过isInterrupted()检查这个标记,来决定要不要退出:ThreadthnewThread(()-{ThreadcurThread.currentThread();while(!cur.isInterrupted()){// 没被中断就一直跑System.out.println(hello thread);Thread.sleep(10_000);}});th.start();ScannerscannernewScanner(System.in);while(!th.isInterrupted()scanner.hasNextInt()){intnscanner.nextInt();if(n0){th.interrupt();// 发中断信号}}这里线程每轮循环前检查isInterrupted(),没被中断就一直打印。主线程一收到 0,就调th.interrupt()挂上中断标记,子线程循环条件不满足了,自然就停了。要注意interrupt()的脾气:如果线程正卡在sleep()/wait()/join()这类会阻塞的操作里,你调interrupt()不会立刻把它从睡觉中叫醒——而是会给它抛一个InterruptedException把它打断。所以在子线程的 sleep 里 catch 到这个异常,就意味着我被中断了。像 demo8 里就抛了个自定义异常来表示被中断:catch(InterruptedExceptione){thrownewInterruptionException(线程被中断);}那个InterruptionException是我自己定义的 RuntimeException:publicclassInterruptionExceptionextendsRuntimeException{publicInterruptionException(Stringmessage){super(message);}}四、线程等待:join很多时候我们有这样的需求:主线程想等子线程把活干完,再继续往下走。比如让一个线程算 1 到 1000 的和,算完主线程再用结果。如果主线程不等待,可能子线程还没算完,主线程就读到 0 了。这时候用join():publicstaticintsum0;publicstaticThreadthnewThread(()-{for(inti1;i1000;i){sumi;}});publicstaticvoidmain(String[]args)throwsInterruptedException{th.start();th.join();// 主线程等在这里,直到 th 线程结束System.out.println(sum);}注释写得很形象——“线程等待,等到 th 线程结束后,执行 main 线程”。join的意思就是合流:我在这等你,你干完我才走。这样最后打印的 sum 一定是子线程算完的 500500,不会打早了。反过来,join也能让新线程等主线程。demo10 里有个有意思的用法——子线程里 join 主线程:ThreadcurmainThread.currentThread();// 拿到 main 的线程对象ThreadthnewThread(()-{curmain.join();// 子线程等 main 结束System.out.println(result);});th.start();for(inti1;i1000;i){resulti;}结果就是子线程要等 main 线程把累加跑完才执行打印。join还能带超时,比如th.join(100_000)表示最多等 10 秒,等不到就算了,不会死等到底。顺带科普个线程状态:调了join后,等待的那个线程就会进入WAITING(死等)或TIMED_WAITING(带超时的等)状态。这就是为什么有人说线程有死等这么个状态,join不带参数就是死等,带了参数就变成有时间限制的等。五、获取当前线程实例在多线程里,代码本身不知道自己是哪个线程在跑,这时候需要Thread.currentThread()——静态方法,谁调用它,它就返回谁。它是获取当前线程最常用的方式,上面好几处都用到了:ThreadcurThread.currentThread();// 拿到当前正在执行这行代码的线程拿到当前线程对象后,能做的事就多了:拿名字:System.out.println(Thread.currentThread().getName());拿线程自己的 id(每个线程有个唯一的 id):Threadt1newThread(()-{// ...},线程1);t1.start();System.out.printf(String.valueOf(t1.getId()));判断当前线程是否被中断,就是上一节中断里那种写法:ThreadcurThread.currentThread();while(!cur.isInterrupted()){// ...}查当前线程处于什么状态(getState()返回 NEW、RUNNABLE、WAITING 那些):ThreadcurThread.currentThread();System.out.println(cur.getState());小结把 Thread 最基础的几件小事串起来看,其实就一个核心:一个线程 一堆要执行的代码(run) 被系统调度的能力(start)。围绕它:创建线程,就是在给 run 塞代码,写法从继承到 lambda 越来越简洁;sleep让它睡,interrupt给它发中断信号让它自己判断停不停,join让别的线程等它干完;currentThread()是时刻要知道我现在是哪个线程的抓手。至于后面更深的线程安全、锁、volatile、wait/notify、线程池,那都是在这套基础之上解决的问题了。基础这关过了,后面就好办多了。