线程介绍与使用(合并)
Phase 5(per
docs/plans/2026-07-01-1500-content-consolidation-and-home-fixes.md):把原1-线程介绍.md(线程基础概念)与2-线程使用.md(线程创建与使用)合并到本文件。前者已迁至note/archive/2026-07-01-合并/。
第一部分:线程基础
一、线程和进程
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
线程个数最优公式 N(thread) = (1 + T(io)/T(cpu)) * Ncpu
进程是操作系统资源分配的基本单位,线程是任务调度和执行的基本单位,进程维护资源,线程是真正的执行体。
包含关系:一个进程可以创建多个线程,且最少一个主线程,线程只能属于一个进程,线程是进程的一部分。
二、上下文切换
CPU 为线程分配时间片,时间片非常短(毫秒级别),CPU 不停地切换线程执行,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。上下文的频繁切换会带来一定的性能开销。
如何减少上下文切换的开销?
- 无锁并发编程:多线程竞争锁时,会引起上下文切换;可以用一些办法来避免使用锁,如将数据的 ID 按照 Hash 算法取模分段,不同的线程处理不同段的数据。
- CAS:Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。
- 使用最少线程:避免创建不需要的线程,比如任务很少却创建了很多线程,造成大量线程都处于等待状态。
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换(主要是 Go 语言)。
三、线程的状态
操作系统的状态:
| 状态 | 含义 |
|---|---|
| NEW | 该状态下的线程还没开始执行 |
| READY | 线程等待获取 CPU 的时间片 |
| RUNNING | 线程正在运行 |
| WAITING | 线程等待 |
| TERMINATING | 该状态下的线程已经退出 |
Java 的状态:
| 状态 | 含义 |
|---|---|
| NEW | 该状态下的线程还没开始执行 |
| RUNNABLE | 该线程正运行中 |
| BLOCKED | 该线程被阻塞,主要等待 synchronized 语句块的锁 |
| WAITING | 无限期等待另外一条线程执行特定的操作(执行了 object.wait()、join()、LockSupport.part()) |
| TIMED_WAITING | 在特定时间内等待另外一条线程执行某种操作(执行了 Object.wait(long)、Thread.join()、LockSupport.parkNanos()、LockSupport.parkUtil),会在特定时间内自行返回 |
| TERMINATED | 该状态下的线程已经退出 |
四、忙循环
忙循环用一个循环让一个线程等待,不像 wait、sleep 会放弃 CPU 的控制权,而是一直在执行当前的程序,这是为了保留 CPU 的缓存。在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间可以使用忙循环。
五、线程挂起
线程的挂起是为了使线程进入"非可执行"状态,这个状态下的线程 CPU 不会分给线程时间片,进入这个状态用来暂停一个线程的运行。
为什么要挂起:CPU 分配的时间片非常短,同时也非常珍贵,为了避免资源的浪费。
挂起线程的操作:
- 废弃的方法:
thread.suspend()该方法不会释放资源,可能会导致死锁,所以废弃;thread.resume()跟着 suspend() 存在,也跟着废弃。 - 可以使用的方法:
wait()方法可以挂起线程,会释放资源(包括锁和 CPU 资源),notify()可以唤醒一个该锁的线程,notifyAll()是唤醒所有锁的线程。这几个方法必须持有锁对象,必须在同步的方法体内。
使用场景:当所有资源没有准备到位的时候,需要挂起等到所有资源都拿到了再执行。
六、线程中断
stop() 方法已经被废弃,因为该方法会强行关闭线程,会导致线程安全问题。
使用 interrupt() 方法可以使线程中断,但是要视情况而定:不使用线程池时直接使用该方法;使用线程池时需要根据标记位去判断线程的状态。
/**
* @Description 中断线程
*/
public class InterruptThread {
/**
* 要保证原子性操作,如果不使用原子操作的类,需要加 volatile
*/
private static AtomicBoolean interrupt = new AtomicBoolean(true);
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = new ThreadPoolExecutor(0, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>());
//手动中断线程的方法
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("手动的线程正在运行");
}
System.out.println("手动的线程结束");
});
thread.start();
//线程池中断
executorService.execute(() -> {
while (interrupt.get()) {
System.out.println("线程池的线程正在运行");
}
System.out.println("线程池的线程结束");
});
Thread.sleep(1L);
//线程中断
thread.interrupt();
//让线程池里的这个线程结束
interrupt.set(false);
}
}七、线程优先级
线程优先级的高低跟数字的大小相反,优先级为 1 最高,10 最低。
线程的优先级告诉程序线程的重要程度,当线程堵塞的时候,程序会尽可能先运行优先级比较高的线程,优先级比较低的线程被准许运行的概率会比较低。
八、守护线程
线程分为用户线程和守护线程。守护线程是指整个程序中所有用户线程的守护者,只要有活着的用户线程,守护线程就活着。当 JVM 中最后一个非守护线程结束时,守护线程也跟着 JVM 一起退出。
尽量少使用守护线程,因为不可控制。不要在守护线程当中进行读写操作、执行计算逻辑等等。
第二部分:线程使用
九、Runnable 方法的定义
Runnable 接口为关联 Thread 对象的线程提供执行代码。这些代码放在 Runnable 的 run 方法当中,这个方法没有返回值但是可能会抛出异常。
十、手动创建线程的方式
1. 继承 Thread 类创建线程
public class MyThread extends Thread {
public void run() {
// 重写 run 方法
}
}2. 实现 Runnable 接口创建线程
public class MyThread2 implements Runnable {
public void run() {
// 重写 run 方法
}
}3. 使用 Callable 和 Future 创建线程
CallableTest callableTest = new CallableTest();
// 因为 Callable 接口是函数式接口,可以使用 Lambda 表达式
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)() -> {
int i = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "的循环变量i的值:" + i);
}
return i;
});
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "的循环变量i:" + i);
if (i == 20) {
new Thread(task, "有返回值的线程").start();
}
}
try {
System.out.println("子线程返回值:" + task.get());
} catch (Exception e) {
e.printStackTrace();
}4. 使用线程池(Executor 框架)
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
// ExecutorService executorService = Executors.newFixedThreadPool(5);
// ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
executorService.execute(new TestRunnable());
System.out.println("************* a" + i + " *************");
}
executorService.shutdown();
}
class TestRunnable implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName() + "线程被调用了。");
}
}十一、Spring Boot 创建线程池
线程池配置文件:
/**
* @Description 线程池配置文件
*/
@Configuration
@EnableAsync
public class ThreadPoolConfig {
@Bean
public ThreadPoolTaskExecutor defaultThreadPool() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
// 核心线程数量
threadPoolTaskExecutor.setCorePoolSize(2);
// 最大线程数量
threadPoolTaskExecutor.setMaxPoolSize(5);
// 队列中最大任务数
threadPoolTaskExecutor.setQueueCapacity(2);
// 线程名称前缀
threadPoolTaskExecutor.setThreadNamePrefix("ThreadPool-");
// 当达到最大线程数时如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 线程空闲后最大存活时间
threadPoolTaskExecutor.setKeepAliveSeconds(60);
// 初始化线程池
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}线程池使用:
@Autowired
private ThreadPoolTaskExecutor threadPool;
public void test() {
threadPool.execute(() -> {
System.out.println("已经发送过短信");
});
}