43. 【Java】并发编程入门:线程的创建与生命周期
摘要本文是Java并发编程的入门指南从进程与线程的基本概念讲起详细介绍了Java中创建线程的三种方式继承Thread类、实现Runnable接口、实现Callable接口深入解析线程的生命周期与六种状态并讲解了控制线程的关键方法start、sleep、join、yield、interrupt。同时涵盖了守护线程、线程安全性问题以及JDK 21虚拟线程的预告为后续深入学习并发编程打下坚实基础。关键词Java并发编程, 线程创建, 线程生命周期, 线程控制方法, 线程安全在之前的文章中我们写的程序都是“单线程”的——从main方法开始一行一行地执行一个方法调用完再调用下一个。就像一个人在做一件事情做完一件再做下一件。这种模式简单、可靠但有一个问题慢。如果你的程序需要同时处理多件事情——比如一个 Web 服务器要同时响应成千上万个用户的请求一个下载工具要同时下载多个文件一个游戏要同时处理画面渲染和用户输入——单线程就远远不够了。这时候你需要并发编程Concurrent Programming。在 Java 中并发编程的基本单元是线程Thread。今天我们就从最基础的地方开始什么是进程什么是线程Java 中如何创建线程线程有哪些状态如何控制线程的执行1. 进程与线程 —— 从“工厂”到“工人”在聊线程之前我们得先搞清楚两个基本概念进程和线程。进程Process是操作系统中一个正在运行的程序实例。比如你打开一个浏览器操作系统就为它分配了一块独立的内存空间启动了一个进程你打开一个文本编辑器又是一个独立的进程。进程之间是相互隔离的一个进程崩溃了一般不会影响其他进程。线程Thread是进程内部的“执行单元”。一个进程可以包含多个线程它们共享进程的内存空间比如堆内存但每个线程有自己的栈和程序计数器。打个比方进程就像一家工厂工厂有独立的厂房、设备、原料仓库。线程就像是工厂里的工人。一个工厂可以有多个工人他们共享工厂的设备和原料共享内存但每个工人有自己的工具箱和任务清单栈和程序计数器。如果工厂倒闭了进程结束所有工人也都没了。进程 vs 线程对比维度进程线程资源开销大独立内存空间小共享进程内存切换开销大小通信方式进程间通信IPC复杂共享内存简单隔离性强一个崩溃不影响其他弱一个线程崩溃可能影响整个进程Java 中的表示没有直接对应的类java.lang.Thread在 Java 中我们主要和线程打交道。一个 Java 程序启动时JVM 会创建一个主线程main线程然后你可以创建更多的线程来并发执行任务。2. Java 中创建线程的三种方式在 Java 中创建一个线程并让它执行任务主要有三种方式。2.1 方式一继承Thread类这是最直接的方式创建一个类继承java.lang.Thread然后重写run()方法。// 1. 继承 ThreadclassMyThreadextendsThread{Overridepublicvoidrun(){// 这个线程要执行的任务for(inti0;i5;i){System.out.println(子线程执行中i);try{Thread.sleep(500);// 暂停 500 毫秒}catch(InterruptedExceptione){e.printStackTrace();}}}}publicclassThreadDemo1{publicstaticvoidmain(String[]args){// 2. 创建线程对象MyThreadtnewMyThread();// 3. 启动线程调用 start()而不是 run()t.start();// 主线程也做点事情for(inti0;i5;i){System.out.println(主线程执行中i);try{Thread.sleep(300);}catch(InterruptedExceptione){e.printStackTrace();}}}}运行结果每次可能略有不同主线程执行中0 子线程执行中0 子线程执行中1 主线程执行中1 主线程执行中2 子线程执行中2 ...注意两个线程是交替执行的顺序由操作系统调度决定。关键点必须调用start()来启动线程而不是run()。如果直接调用run()那只是在当前线程中执行这个方法不会创建新线程。2.2 方式二实现Runnable接口由于 Java 是单继承如果你的类已经继承了其他类就不能再继承Thread。这时候你可以实现Runnable接口。// 1. 实现 Runnable 接口classMyRunnableimplementsRunnable{Overridepublicvoidrun(){for(inti0;i5;i){System.out.println(Runnable 线程i);try{Thread.sleep(500);}catch(InterruptedExceptione){e.printStackTrace();}}}}publicclassThreadDemo2{publicstaticvoidmain(String[]args){// 2. 创建 Runnable 实例MyRunnabletasknewMyRunnable();// 3. 用 Runnable 创建 Thread 对象ThreadtnewThread(task);// 4. 启动t.start();// 也可以用 Lambda 表达式更简洁Threadt2newThread(()-{for(inti0;i5;i){System.out.println(Lambda 线程i);try{Thread.sleep(400);}catch(InterruptedExceptione){e.printStackTrace();}}});t2.start();}}Runnable的方式比继承Thread更灵活也是更推荐的方式因为它把“任务”和“执行任务的线程”分离开了。2.3 方式三实现Callable接口带返回值Runnable的run()方法没有返回值也不能抛出受检异常。如果你需要让线程执行后返回一个结果可以用CallableV接口。Callable和Runnable类似但它的call()方法有返回值并且可以抛出异常。不过Callable不能直接传给Thread需要配合FutureTask来使用。importjava.util.concurrent.Callable;importjava.util.concurrent.ExecutionException;importjava.util.concurrent.FutureTask;publicclassThreadDemo3{publicstaticvoidmain(String[]args){// 1. 创建 Callable 任务CallableIntegertask()-{intsum0;for(inti1;i10;i){sumi;Thread.sleep(100);}returnsum;// 返回计算结果};// 2. 用 FutureTask 包装 CallableFutureTask 实现了 RunnableFutureTaskIntegerfutureTasknewFutureTask(task);// 3. 创建线程并启动ThreadtnewThread(futureTask);t.start();// 4. 主线程可以做其他事情...System.out.println(主线程在做其他事...);// 5. 获取子线程的执行结果会阻塞直到子线程完成try{IntegerresultfutureTask.get();// 阻塞等待System.out.println(1 到 10 的和是result);}catch(InterruptedException|ExecutionExceptione){e.printStackTrace();}}}CallableFutureTask组合让你能够在线程执行完成后获取返回值。通过futureTask.get()阻塞等待结果。可以取消任务futureTask.cancel(true)。在实际开发中我们很少直接用Thread类来创建线程而是使用线程池ExecutorService。我们会在后面的文章中详细讲线程池但现在先掌握线程的基本创建方式。3. 线程的生命周期与状态一个线程从创建到死亡会经历几个不同的状态。Java 中线程的状态定义在Thread.State枚举中NEW新建 ↓ start() RUNNABLE可运行 ↓ 等待CPU调度 RUNNING运行中但 RUNNABLE 包含了运行和就绪 ↓ 调用 wait() / join() / sleep() BLOCKED / WAITING / TIMED_WAITING阻塞 ↓ 条件满足 RUNNABLE ↓ run() 执行完毕 / 异常退出 TERMINATED终止实际上Thread.State定义了六种状态状态说明NEW线程对象被创建但还没有调用start()RUNNABLE线程可运行正在等待 CPU 时间片包括运行中和就绪BLOCKED线程被阻塞正在等待一个锁synchronized锁WAITING线程无限期等待另一个线程执行特定操作如wait()、join()无超时TIMED_WAITING线程在指定时间内等待如sleep(ms)、wait(timeout)、join(timeout)TERMINATED线程已终止run()执行完毕或异常退出我们可以用Thread.getState()来查看线程的当前状态ThreadtnewThread(()-{System.out.println(线程运行中...);try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}});System.out.println(创建后t.getState());// NEWt.start();System.out.println(启动后t.getState());// RUNNABLE也可能很快变成 TIMED_WAITINGThread.sleep(200);System.out.println(sleep 中t.getState());// TIMED_WAITING大概t.join();System.out.println(结束后t.getState());// TERMINATED理解线程状态的变化对于调试并发程序和避免死锁非常重要。4. 控制线程的关键方法4.1start()—— 启动线程创建线程对象后必须调用start()才会启动新线程JVM 会调用该线程的run()方法。4.2run()—— 线程执行体run()方法里放的是线程要执行的任务代码。不要直接调用run()应该由 JVM 在start()后自动调用。4.3sleep(long millis)—— 让线程休眠让当前线程暂停执行指定的毫秒数让出 CPU 给其他线程。它不会释放锁。Thread.sleep(1000);// 休眠 1 秒sleep()会抛出InterruptedException受检异常必须捕获或声明。4.4join()—— 等待线程结束让当前线程等待另一个线程执行完毕。ThreadtnewThread(()-{// 做一些耗时操作System.out.println(子线程工作完成);});t.start();t.join();// 主线程会在这里等待直到 t 执行完毕System.out.println(主线程继续);join()也有带超时参数的版本join(long millis)。4.5yield()—— 主动让出 CPUyield()是一个“谦让”的提示告诉调度器当前线程愿意让出 CPU让其他线程有机会执行。但它只是一个建议调度器可能忽略它。Thread.yield();// 当前线程让出 CPU它很少在实际开发中使用通常只用于测试或调试。4.6interrupt()—— 中断线程interrupt()用于向一个线程发送“中断信号”。它不会直接停止线程而是设置线程的中断状态标志。线程需要自己检查这个标志并决定如何响应。ThreadtnewThread(()-{while(!Thread.currentThread().isInterrupted()){// 执行任务...System.out.println(工作中...);try{Thread.sleep(500);}catch(InterruptedExceptione){// 如果在 sleep 中被中断会抛出 InterruptedException// 此时中断状态会被清除需要重新设置Thread.currentThread().interrupt();// 重新设置中断状态break;}}System.out.println(线程被中断退出);});t.start();Thread.sleep(2000);t.interrupt();// 发送中断信号interrupt()是 Java 中优雅地终止线程的推荐方式而不是使用已废弃的stop()方法。5. 守护线程 —— 默默服务的“仆人”Java 中有两种线程用户线程User Thread和守护线程Daemon Thread。用户线程JVM 会等待所有用户线程执行完毕才退出。守护线程JVM 不会等待守护线程当所有用户线程结束时守护线程会被强制终止不管有没有执行完。守护线程通常用于在后台执行服务性任务比如垃圾回收GC、日志记录、监控等。ThreadtnewThread(()-{while(true){System.out.println(守护线程在后台运行...);try{Thread.sleep(1000);}catch(InterruptedExceptione){break;}}});t.setDaemon(true);// 设置为守护线程必须在 start() 之前调用t.start();// 主线程只运行 3 秒Thread.sleep(3000);System.out.println(主线程结束JVM 退出守护线程会被强制终止);6. 线程安全性 —— 一个危险的例子我们先看一个简单的例子体会一下“多个线程共享数据”时可能出现的问题publicclassCounter{privateintcount0;publicvoidincrement(){count;}publicintgetCount(){returncount;}publicstaticvoidmain(String[]args)throwsInterruptedException{CountercounternewCounter();// 创建 1000 个线程每个线程对 count 加 1Thread[]threadsnewThread[1000];for(inti0;i1000;i){threads[i]newThread(counter::increment);threads[i].start();}// 等待所有线程结束for(Threadt:threads){t.join();}// 预期结果是 1000但实际可能不是System.out.println(最终 count counter.getCount());}}你运行几次可能会得到 1000也可能得到 998、999、1000 等不同的结果。为什么因为count不是原子操作它包含三个步骤读取 count → 加 1 → 写回 count。多个线程同时操作会产生数据竞争race condition。这就是并发编程中最核心的问题线程安全。我们会在接下来的文章中详细讲解如何解决这个问题synchronized、Lock、Atomic类等。7. JDK 21 的虚拟线程 —— 预告在 JDK 21 中最重磅的特性就是虚拟线程Virtual Threads。传统的线程现在称为“平台线程”由操作系统内核管理创建和切换开销较大。而虚拟线程是由 JVM 管理的轻量级线程创建成本极低可以轻松创建成千上万个。我们将在本阶段的后半部分专门用两篇文章来深入讲解虚拟线程。现在你只需要知道Java 的并发编程正在进入一个新时代。8. 今天的总结今天我们迈出了并发编程的第一步进程 vs 线程进程是程序实例线程是进程内的执行单元。三种创建线程的方式继承Thread类简单但单继承限制实现Runnable接口更灵活推荐实现Callable接口 FutureTask带返回值线程的六种状态NEW→RUNNABLE→BLOCKED/WAITING/TIMED_WAITING→TERMINATED。常用方法start()、sleep()、join()、yield()、interrupt()。守护线程在后台默默服务JVM 不会等待它结束。线程安全问题多个线程共享数据时可能产生数据竞争需要用同步机制来保证安全。并发编程是一个非常深广的领域今天我们只是刚刚推开门。你知道了怎么创建线程、怎么控制线程、怎么让线程协作。在接下来的几篇文章中我们会深入探讨线程安全、同步机制、并发工具类和虚拟线程。动手试试用三种方式分别创建线程让它们打印 1 到 10 的数字观察输出顺序是否每次都一样。写一个程序创建两个线程一个线程打印奇数1、3、5…另一个线程打印偶数2、4、6…让它们交替输出 1 到 100。提示可以用wait()和notify()或者用join()控制但注意交替打印需要同步我们下一篇文章才会讲你可以先尝试用sleep()控制顺序虽然不完美但能观察现象。用join()确保主线程等待子线程执行完毕后再继续。创建一个守护线程让它在后台每隔 1 秒打印一次“心跳”主线程运行 5 秒后退出观察守护线程是否也随之终止。运行上面Counter的例子多次运行观察结果是否总是 1000如果不是你能猜到为什么吗先不用解决我们下一篇文章会讲解决方案。我们下一篇见。 获取本系列示例代码请访问 GitCode。