深入 JUC 入门核心:Java 多线程创建与运行机制全解析(Java 实习生必修课)
适用人群
- 计算机科学与技术、软件工程等专业的在校本科生或研究生,正在学习《操作系统》《并发编程》等课程;
- Java 初级开发者或实习生,希望系统掌握多线程基础与 JUC(java.util.concurrent)入门知识;
- 准备 Java 后端岗位面试,需深入理解线程创建方式、生命周期、启动原理及常见误区;
- 对高并发、异步编程、线程池等高级特性感兴趣的开发者。
本文假设读者已掌握 Java 基础语法(类、接口、Lambda 表达式),并对“进程”“线程”等操作系统概念有初步了解。内容由浅入深,兼顾理论深度与工程实践,适合从“单线程编程”迈向“并发编程”的进阶学习者。
关键词
JUC、Java 并发、多线程、Thread、Runnable、Callable、线程创建、线程启动、start() vs run()、线程生命周期、线程状态、守护线程、线程优先级、Java 实习生、计算机专业核心课、JUC 入门、并发编程基础、操作系统线程模型、JVM 线程映射。
引言:为什么“创建线程”是并发编程的第一道门槛?
在单线程世界里,程序按顺序执行,逻辑清晰、调试简单。但一旦进入多线程领域,“如何正确创建并启动一个线程”就成了无数初学者的“绊脚石”。
你是否曾遇到过以下困惑?
- 为什么重写
run()方法后直接调用它,却没有并发效果? Thread和Runnable到底有什么区别?该用哪一个?- 为什么主线程结束,子线程有时还在运行,有时却突然终止?
Callable和Future是什么?它们和Runnable有何不同?
这些问题的答案,都藏在Java 线程的创建与运行机制中。作为 JUC(java.util.concurrent)并发包的基石,掌握线程的正确创建方式,是理解后续线程池、锁、原子类、并发集合等高级特性的前提。
本文将从操作系统视角 → JVM 实现 → Java API 层三层递进,全面剖析:
- 线程的本质:OS 线程 vs JVM 线程;
- 四种线程创建方式:从
Thread到CompletableFuture; start()与run()的本质区别(附字节码分析);- 线程的生命周期与六种状态;
- 守护线程、优先级等关键属性;
- 常见误区与最佳实践。
全文超过 9000 字,包含大量代码示例、图解、源码片段与面试高频问题,助你彻底打通多线程入门关!
一、线程的本质:从操作系统到 JVM
1.1 什么是线程?
在操作系统中,线程(Thread)是CPU 调度的基本单位,而进程(Process)是资源分配的基本单位。一个进程可包含多个线程,它们共享进程的内存空间(堆、方法区),但拥有独立的栈(Stack)。
📌关键特性:
- 轻量级:创建/切换线程比进程开销小;
- 共享内存:线程间通信高效,但也带来数据竞争(Race Condition)风险;
- 并发执行:多核 CPU 可真正并行;单核 CPU 通过时间片轮转实现“伪并行”。
1.2 JVM 如何映射 OS 线程?
Java 线程并非 JVM 自己实现的“用户态线程”,而是直接映射到操作系统的内核线程(Kernel Thread),即1:1 模型。
| 层级 | 实体 | 说明 |
|---|---|---|
| 应用层 | java.lang.Thread | Java 对象,封装线程行为 |
| JVM 层 | JNI 调用 | 调用 OS API(如 pthread_create on Linux) |
| OS 层 | Kernel Thread | 真正被 CPU 调度的实体 |
✅优势:
- 利用 OS 的调度优化(如多核亲和性);
- 阻塞操作(如 I/O)不会阻塞整个 JVM。
⚠️代价:
- 每个线程默认占用1MB 栈空间(可通过
-Xss调整);- 创建过多线程会导致内存溢出(OOM)或上下文切换开销剧增。
二、四种创建线程的方式:从基础到现代
2.1 方式一:继承 Thread 类(不推荐)
publicclassMyThreadextendsThread{@Overridepublicvoidrun(){System.out.println("Thread running: "+Thread.currentThread().getName());}}// 使用publicclassMain{publicstaticvoidmain(String[]args){MyThreadt=newMyThread();t.start();// 注意:必须调用 start()!}}优点:简单直观。
缺点:
- Java 不支持多继承,若类已继承其他父类,则无法再继承
Thread; - 将线程逻辑与任务逻辑耦合在一起,违反“组合优于继承”原则。
❌反模式:直接调用
t.run()—— 这只是普通方法调用,不会启动新线程!
2.2 方式二:实现 Runnable 接口(推荐)
publicclassMyTaskimplementsRunnable{@Overridepublicvoidrun(){System.out.println("Task running in: "+Thread.currentThread().getName());}}// 使用publicclassMain{publicstaticvoidmain(String[]args){Threadt=newThread(newMyTask());t.start();// 或使用 Lambda(更简洁)newThread(()->System.out.println("Lambda task")).start();}}优点:
- 解耦:任务(
Runnable)与线程(Thread)分离; - 灵活:同一个
Runnable实例可被多个线程执行; - 符合面向接口编程。
✅最佳实践:优先使用
Runnable+Thread组合。
2.3 方式三:实现 Callable 接口 + FutureTask(获取返回值)
Runnable的run()方法无返回值、不能抛出受检异常。若需返回结果或处理异常,应使用Callable。
importjava.util.concurrent.*;publicclassMyCallableimplementsCallable<String>{@OverridepublicStringcall()throwsException{return"Result from "+Thread.currentThread().getName();}}// 使用publicclassMain{publicstaticvoidmain(String[]args)throwsException{Callable<String>task=newMyCallable();FutureTask<String>futureTask=newFutureTask<>(task);Threadt=newThread(futureTask);t.start();// 获取结果(会阻塞直到任务完成)Stringresult=futureTask.get();System.out.println(result);}}关键组件:
Callable<V>:类似Runnable,但call()可返回值、抛异常;FutureTask<V>:包装Callable,实现Runnable接口,可被Thread执行;Future<V>:提供get()、isDone()、cancel()等方法管理异步任务。
💡注意:
futureTask.get()是阻塞调用,若任务未完成,主线程会一直等待。
2.4 方式四:使用线程池(ExecutorService)——生产首选
手动创建Thread存在资源浪费、缺乏管理、易 OOM等问题。JUC 提供线程池抽象,统一管理线程生命周期。
importjava.util.concurrent.*;publicclassThreadPoolExample{publicstaticvoidmain(String[]args){// 创建固定大小线程池ExecutorServiceexecutor=Executors.newFixedThreadPool(2);// 提交 Runnable 任务executor.submit(()->System.out.println("Runnable task"));// 提交 Callable 任务Future<String>future=executor.submit(()->"Callable result");try{System.out.println(future.get());}catch(Exceptione){e.printStackTrace();}// 关闭线程池executor.shutdown();}}优势:
- 复用线程:避免频繁创建/销毁开销;
- 控制并发数:防止系统过载;
- 任务队列:缓冲突发请求;
- 统一管理:优雅关闭、监控等。
✅生产环境准则:永远不要直接 new Thread(),而应使用线程池!
三、start()与run()的本质区别:字节码级剖析
这是面试高频题,也是初学者最易混淆的概念。
3.1 表象差异
Threadt=newThread(()->System.out.println("Hello"));t.run();// 输出 "Hello",但仍在主线程执行t.start();// 输出 "Hello",在新线程执行3.2 源码揭秘
查看Thread.java(JDK 8):
// run() 方法:只是一个普通方法@Overridepublicvoidrun(){if(target!=null){target.run();// target 即传入的 Runnable}}// start() 方法:触发 JVM 创建新线程publicsynchronizedvoidstart(){if(threadStatus!=0)thrownewIllegalThreadStateException();// 添加到线程组group.add(this);booleanstarted=false;try{start0();// native 方法!started=true;}finally{if(!started)group.threadStartFailed(this);}}// native 方法,由 JVM 实现privatenativevoidstart0();3.3 字节码与执行流程
调用
run():- JVM 直接在当前线程栈帧中执行
run()方法; - 无新线程创建,等同于普通方法调用。
- JVM 直接在当前线程栈帧中执行
调用
start():- JVM 通过 JNI 调用 OS API(如
pthread_create)创建内核线程; - 新线程启动后,自动回调
run()方法; - 此时
run()在新线程的栈中执行。
- JVM 通过 JNI 调用 OS API(如
📊流程图:
主线程 │ ├─ t.run() → 直接执行 run()(主线程栈) │ └─ t.start() ↓ [JVM] → [OS] 创建新线程 ↓ 新线程 → 自动调用 t.run()(新线程栈)
3.4 常见错误:重复调用 start()
Threadt=newThread(()->{});t.start();t.start();// 抛出 IllegalThreadStateException!原因:start()方法开头检查threadStatus != 0。线程一旦启动,状态变为非 0,再次调用即非法。
✅线程不可复用:一个
Thread对象只能启动一次。
四、线程的生命周期与六种状态(Thread.State)
Java 线程并非只有“运行”和“停止”两种状态,而是有六种明确的状态(自 JDK 5 起定义于Thread.State枚举)。
| 状态 | 说明 | 触发条件 |
|---|---|---|
| NEW | 新建 | new Thread()后,未调用start() |
| RUNNABLE | 可运行 | 已调用start(),等待 CPU 调度(含正在运行) |
| BLOCKED | 阻塞 | 等待获取 synchronized 锁 |
| WAITING | 等待 | 调用wait()、join()、LockSupport.park() |
| TIMED_WAITING | 超时等待 | 调用sleep(time)、wait(time)、join(time) |
| TERMINATED | 终止 | run()方法执行完毕或异常退出 |
4.1 状态转换图
+--------+ | NEW | +---+----+ | start() v +------+-------+ | RUNNABLE |<------------------+ +------+-------+ | | | +-------v-------+ +-------------v-------------+ | BLOCKED | | WAITING / | +-------+-------+ | TIMED_WAITING | | +-------------+-------------+ | | acquire lock notify()/interrupt()/timeout | | +------------->-------------+ | run() ends v +-------+--------+ | TERMINATED | +----------------+4.2 代码演示状态变化
publicclassThreadStateDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt=newThread(()->{try{Thread.sleep(2000);// TIMED_WAITING}catch(InterruptedExceptione){e.printStackTrace();}});System.out.println(t.getState());// NEWt.start();System.out.println(t.getState());// RUNNABLE(可能短暂)Thread.sleep(100);System.out.println(t.getState());// TIMED_WAITINGt.join();// 主线程 WAITING,直到 t 结束System.out.println(t.getState());// TERMINATED}}🔍注意:
RUNNABLE包含就绪(Ready)和运行(Running)两种 OS 状态,JVM 不区分。
五、线程的关键属性:守护线程与优先级
5.1 守护线程(Daemon Thread)
定义:为其他线程提供服务的线程。当所有非守护线程结束时,JVM 会强制终止所有守护线程并退出。
典型应用:
- GC 线程;
- 心跳检测线程;
- 日志 flush 线程。
设置方式:
Threadt=newThread(()->{while(true){System.out.println("Daemon running...");try{Thread.sleep(1000);}catch(InterruptedExceptione){}}});t.setDaemon(true);// 必须在 start() 前设置!t.start();// 主线程结束 → JVM 退出,守护线程被强制终止System.out.println("Main thread ends.");⚠️重要规则:
setDaemon(true)必须在start()之前调用,否则抛IllegalThreadStateException;- 守护线程中不应执行关键业务逻辑(可能被突然终止)。
5.2 线程优先级(Priority)
Java 提供 1~10 的优先级(Thread.MIN_PRIORITY=1,MAX_PRIORITY=10,NORM_PRIORITY=5),但仅作为建议,实际调度由 OS 决定。
Threadt1=newThread(()->{/* 高优先级任务 */});Threadt2=newThread(()->{/* 低优先级任务 */});t1.setPriority(Thread.MAX_PRIORITY);t2.setPriority(Thread.MIN_PRIORITY);t1.start();t2.start();⚠️现实情况:
- Linux 默认忽略 Java 优先级;
- Windows 有一定效果,但不保证;
- 不要依赖优先级控制执行顺序!应使用同步机制(如
CountDownLatch)。
六、常见误区与最佳实践
误区一:“调用 run() 就是启动线程”
纠正:run()只是普通方法调用,不会创建新线程。必须调用start()。
误区二:“线程可以复用”
纠正:一个Thread对象只能启动一次。若需多次执行任务,应:
- 使用
Runnable+ 新Thread; - 或使用线程池。
误区三:“守护线程可以做重要工作”
纠正:守护线程会在 JVM 退出时被强制终止,不能用于写文件、发消息等关键操作。
误区四:“高优先级线程一定先执行”
纠正:优先级只是 hint,不能保证执行顺序。并发控制应依赖锁、信号量等同步工具。
七、JUC 入门:从 Thread 到 ExecutorService
虽然Thread是基础,但 JUC 的核心思想是“解耦任务提交与执行”。因此,线程池(ExecutorService)才是现代 Java 并发的标准入口。
7.1 Executors 工厂方法(谨慎使用!)
| 方法 | 说明 | 风险 |
|---|---|---|
newFixedThreadPool(n) | 固定大小线程池 | 任务队列无界,可能 OOM |
newCachedThreadPool() | 缓存线程池(60s 超时) | 线程数无界,可能 OOM |
newSingleThreadExecutor() | 单线程池 | 任务队列无界 |
newScheduledThreadPool(n) | 支持定时任务 | 同上 |
⚠️阿里《Java 开发手册》禁止使用 Executors!
原因:默认使用LinkedBlockingQueue(无界队列),高负载下易导致内存溢出。
7.2 正确创建线程池(推荐)
ExecutorServiceexecutor=newThreadPoolExecutor(2,// corePoolSize4,// maximumPoolSize60L,TimeUnit.SECONDS,// keepAliveTimenewArrayBlockingQueue<>(100),// 有界队列newThreadPoolExecutor.CallerRunsPolicy()// 拒绝策略);参数含义:
corePoolSize:核心线程数(即使空闲也不回收);maximumPoolSize:最大线程数;workQueue:任务队列(必须有界!);RejectedExecutionHandler:拒绝策略(如 CallerRunsPolicy:由提交者线程执行)。
八、学习建议与扩展阅读
8.1 动手实验清单
- 验证 start() vs run():打印线程 ID,观察是否相同;
- 模拟守护线程:主线程 sleep 后退出,观察守护线程行为;
- 线程状态监控:用
jstack <pid>查看线程状态; - 线程池对比:分别用
new Thread()和ThreadPoolExecutor执行 1000 个任务,观察资源消耗。
8.2 推荐资料
- 📘《Java 并发编程实战》(Brian Goetz)
并发领域的圣经,第 5 章详解线程基础。 - 📘《深入理解 Java 虚拟机》— 周志明
第 12 章“Java 内存模型与线程”。 - 📄Oracle Concurrency Tutorial
官方并发教程。 - 🎥Bilibili 视频:
- 尚硅谷《JUC 并发编程》
- 黑马程序员《Java 多线程核心技术》
8.3 面试高频问题
- 创建线程有哪几种方式?各有什么优缺点?
start()和run()的区别是什么?- 线程有哪些状态?如何转换?
- 什么是守护线程?应用场景?
- 为什么不要直接使用 Executors 创建线程池?
九、总结
“创建线程”看似简单,实则蕴含操作系统、JVM、Java API 三层知识。本文系统讲解了:
- 线程的本质:JVM 如何映射 OS 线程;
- 四种创建方式:从
Thread到线程池的演进; start()vsrun():字节码级原理剖析;- 六种线程状态:生命周期完整图谱;
- 守护线程与优先级:关键属性与使用场景;
- JUC 入门指引:为何线程池是生产首选。
最后寄语:
并发编程的旅程,始于正确创建一个线程。
不要满足于“能跑起来”,
而要追问“为什么这样设计”。
从今天起,用jstack观察你的线程,
用线程池管理你的任务,
你将真正踏入高并发的世界。
欢迎在评论区交流!
👉 你在实习中是否遇到过线程相关的问题?
👉 对哪种线程创建方式最感兴趣?
点赞 + 收藏 + 关注,获取更多 JUC 与并发编程干货!🚀