Java 线程基础

笔记/Java/Java基础/Java并发/Java 线程基础

下一篇:线程生命周期与通信 · 面试:多线程面试 59 道

进程与线程#

进程线程
定位OS 资源分配单位CPU 调度执行单位
内存独立地址空间共享进程堆,各自栈与 PC
切换开销
崩溃影响进程隔离,较健壮同进程线程崩溃可能拖垮进程

并行 vs 并发#

  • 并发:单核上多任务交替执行,逻辑上“同时”
  • 并行:多核上真正同时执行
  • 串行:一个线程顺序执行,无竞态

多线程价值:提高多核利用率、避免单线程阻塞、便于任务拆分建模。

先搞清:创建线程到底在干什么#

Java 里「创建线程」分两步,不要混在一起:

  1. 定义任务:这段代码要在后台跑什么(run() 里干什么)
  2. 启动线程:让 JVM 向操作系统申请一条新执行流,并去跑上面的任务

可以记成:

任务(Runnable / Callable) + 载体(Thread / 线程池) → start / submit → 真正并发执行

面试说的「四种方式」,本质是 四种把「任务」交给「线程」的组合;前三种是「自己 new Thread」,第四种是「交给线程池」。


方式一:继承 Thread#

把任务写在 Thread 子类的 run() 里,再 new 出来并 start()

public class DownloadThread extends Thread {
private final String url;
public DownloadThread(String url) {
this.url = url;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 下载: " + url);
// 模拟下载
}
}
// 使用
public class Demo1 {
public static void main(String[] args) {
Thread t = new DownloadThread("https://example.com/a.zip");
t.setName("download-1");
t.start(); // 必须 start,不能只 run()
}
}

内部发生了什么(简化)

  1. new DownloadThread():只是在堆上创建一个 Thread 对象,状态是 NEW还没有 OS 线程
  2. start():JVM 向 OS 创建原生线程,并回调当前对象的 run();状态变为 RUNNABLE
  3. run() 执行完:线程 TERMINATED

优点:写法直观,适合 Demo。
缺点:Java 单继承——类已经 extends Thread 就不能再继承业务父类;任务和线程绑死,无法交给线程池复用同一套任务类。
结论:了解即可,业务代码不推荐。


方式二:实现 Runnable(最常用的一种写法)#

任务单独写成 Runnable,再 new Thread(runnable).start()。Lambda () -> { ... } 就是 Runnable 的简写。

public class DownloadTask implements Runnable {
private final String url;
public DownloadTask(String url) {
this.url = url;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 下载: " + url);
}
}
public class Demo2 {
public static void main(String[] args) {
Runnable task = new DownloadTask("https://example.com/b.zip");
// 等价:Runnable task = () -> System.out.println("lambda 任务");
Thread t = new Thread(task, "download-2");
t.start();
}
}

和方式一的本质区别

继承 Thread实现 Runnable
任务在哪子类 run()独立 Runnable 对象
继承位占用 extends只占 implements,还能继承别的类
同任务多线程要 new 多个 Thread 子类同一个 Runnable 可包进多个 Thread(一般不这么干,但任务可复用)
线程池不友好天然兼容 pool.execute(task)

为什么要分离 Runnable?
线程池、定时任务、框架回调都只认 Runnable/Callable,不认你的 Thread 子类。生产里 任务定义线程调度 应该分开。


方式三:Callable + FutureTask(需要返回值时)#

Runnable.run() 返回 void,也不能在方法签名里抛 checked 异常。需要 结果异常传递 时用 Callable

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class SumTask implements Callable<Integer> {
private final int from, to;
public SumTask(int from, int to) {
this.from = from;
this.to = to;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = from; i <= to; i++) {
sum += i;
}
return sum; // 有返回值
}
}
public class Demo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new SumTask(1, 100);
// FutureTask 既是 Future(拿结果),又是 Runnable(能交给 Thread)
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask, "sum-worker");
t.start();
// 主线程继续干别的,需要结果时再 get(会阻塞直到算完)
System.out.println("主线程先做别的...");
Integer result = futureTask.get();
System.out.println("1+2+...+100 = " + result);
}
}

三个角色别混

类型作用
Callable<V>定义带返回值的任务,call() 代替 run()
FutureTask<V>包装 Callable,同时实现 Runnable + Future
Future<V>只关心结果:get()cancel()isDone()

为什么多一层 FutureTask?
Thread 的构造函数只接受 Runnable,不认识 CallableFutureTaskCallable 适配Runnable,所以能 new Thread(futureTask).start()

get() 注意:阻塞等待子线程结束;子线程里抛出的异常会在 get() 时包装成 ExecutionException 抛给主线程。


方式四:线程池(生产环境默认选项)#

前三种每次 new Thread(...).start() 都会 新建一条 OS 线程,开销大,线程多了还会 OOM。线程池 复用 固定数量的工作线程,任务排队执行。

import java.util.concurrent.*;
public class Demo4 {
public static void main(String[] args) throws Exception {
// 固定 4 个工作线程的池子
ExecutorService pool = Executors.newFixedThreadPool(4);
// 4-1:无返回值 —— execute(Runnable),等价于方式二
pool.execute(() -> System.out.println("execute: " + Thread.currentThread().getName()));
// 4-2:有返回值 —— submit(Callable),等价于方式三
Future<Integer> future = pool.submit(() -> {
Thread.sleep(500);
return 42;
});
System.out.println("submit 结果: " + future.get());
pool.shutdown(); // 不再接新任务,已有任务跑完后关闭
}
}

和裸 Thread 的对比

裸 Thread: 每个任务 → new Thread → start → 跑完销毁
线程池: 任务入队 → 空闲 Worker 取出执行 → Worker 继续接下一个任务
能力new Thread线程池
线程复用
限制并发数难控制core/max 参数
队列排队
统一异常/命名需自己写ThreadFactory 等

阿里规范建议:不要Executors.newFixedThreadPool 等工厂无脑创建,生产用 ThreadPoolExecutor 显式指定队列大小和拒绝策略。细节见 线程池


四种方式一张表#

方式核心写法返回值典型场景
1. 继承 Threadclass T extends Threadstart()教学 Demo
2. 实现 Runnablenew Thread(runnable).start()异步任务、配合线程池
3. Callable + FutureTasknew Thread(new FutureTask<>(callable)).start()子线程计算,主线程 get()
4. 线程池pool.execute / pool.submitsubmit 有线上默认

演进关系:2 是 1 的改进;3 在 2 上加返回值;4 在 2/3 上加复用和管控。


start() 与 run()(每种方式都要遵守)#

Thread t = new Thread(() -> System.out.println("hello"));
t.run(); // ❌ 当前 main 线程里普通调用 run(),没有新线程
t.start(); // ✅ JVM 启动新线程,在新线程里回调 run()

start() 只能调用 一次;重复 start()IllegalStateException


常见疑问#

Q:Lambda () -> {} 属于哪一种?
属于 Runnable(方式二)。若写成 () -> 123 且交给 submit,编译器会推断为 Callable(方式三/四)。

Q:方式三能否不用 FutureTask,直接 new Thread?
不能。Thread 没有 Callable 构造函数,必须经 FutureTask 或线程池 submit 适配。

Q:方式四还算「创建线程」吗?
算。线程池在初始化或扩容时仍会 new Thread(Worker),只是 业务代码 不再每次自己 new。

上下文切换#

线程数常大于 CPU 核数,时间片轮转时会保存/恢复线程上下文(寄存器、PC 等),有 CPU 开销。

一句话总结#

创建线程 = 定义任务 + 启动执行。四种方式从「继承 Thread」演进到「线程池」:任务与线程分离(Runnable)、要结果用 Callable+Future、线上用线程池复用 Worker。无论哪种,都要 start()/submit(),不要直接 run()

文章目录

文章目录