javaEE(2)多线程初阶
1 线程的核心概念
1.1 进程与线程的定义
进程是操作系统进行资源分配的最小单位,一个正在运行的应用程序对应一个独立进程,拥有独立的内存地址空间。
线程是操作系统进行任务调度的最小单位,是进程内部的一条独立执行流。一个进程至少包含一个主线程,同一进程内的多个线程共享进程的内存资源与文件资源。
1.2 进程与线程的核心差异
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 资源定位 | 系统分配资源的最小单位 | 系统调度 CPU 执行的最小单位 |
| 内存空间 | 进程间内存相互隔离,独立寻址 | 同一进程内线程共享堆内存与方法区,各线程私有栈空间与程序计数器 |
| 故障影响 | 单个进程崩溃不影响其他进程 | 单个线程异常可能导致整个进程终止 |
| 调度开销 | 创建、销毁、上下文切换开销较高 | 创建、销毁、切换开销远低于进程 |
1.3 Java线程与操作系统线程的关系
线程是操作系统内核提供的原生能力,Java 标准库中的 java.lang.Thread 类是对操作系统线程 API 的抽象与封装。调用线程对象的start()方法,才会通过操作系统内核创建并启动一条原生线程;重写run()方法仅用于定义线程的执行逻辑,直接调用run()不会创建新线程,代码仍在当前调用线程中串行执行。
2 线程的创建方式
2.1 继承Thread类
通过继承 Thread 类并重写 run() 方法定义线程任务,创建子类实例后调用 start() 启动线程。该方式受 Java 单继承机制限制,适用于线程类无需继承其他父类的场景。
class CustomThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行中");
}
}
public class ThreadCreateDemo1 {
public static void main(String[] args) {
CustomThread thread = new CustomThread();
thread.start();
}
}
2.2 实现Runnable接口
通过实现 Runnable 接口定义任务逻辑,再将任务实例传入 Thread 构造方法。该方式将任务定义与线程控制解耦,支持类继承其他父类,是更推荐的实现方式。
class CustomTask implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行中");
}
}
public class ThreadCreateDemo2 {
public static void main(String[] args) {
Thread thread = new Thread(new CustomTask());
thread.start();
}
}
2.3 匿名内部类实现
对于一次性使用的线程任务,可通过匿名内部类快速创建,无需单独定义实现类。
public class ThreadCreateDemo3 {
public static void main(String[] args) {
// 继承Thread的匿名内部类
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("匿名Thread实现");
}
};
t1.start();
// 实现Runnable的匿名内部类
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名Runnable实现");
}
});
t2.start();
}
}
2.4 Lambda 表达式实现
Runnable 为函数式接口,可通过 Lambda 表达式简化代码,是实际开发中最常用的简洁写法。
public class ThreadCreateDemo4 {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Lambda方式创建线程"));
thread.start();
}
}
3 Thread类核心API
3.1 线程基础属性
| 方法 | 功能描述 |
|---|---|
long getId() | 返回线程的唯一标识 ID |
String getName() | 返回线程名称,主要用于日志调试 |
Thread.State getState() | 返回线程当前的生命周期状态 |
int getPriority() | 返回线程优先级,优先级仅作为调度参考,不保证执行顺序 |
boolean isDaemon() | 判断是否为守护线程;JVM 会在所有非守护线程终止后退出 |
boolean isAlive() | 判断线程是否处于存活状态,NEW 与 TERMINATED 状态返回 false |
boolean isInterrupted() | 判断线程是否被设置中断标记,调用后不清除标记 |
3.2 线程休眠:sleep()
Thread.sleep(long millis) 为静态方法,使当前线程进入限时等待状态,暂停执行指定时长。该方法不释放持有的锁,且实际休眠时长大于等于参数设定值,具体恢复时间由操作系统调度决定。
3.3 线程等待:join()
join()方法使当前调用线程阻塞,直到目标线程执行完毕后再继续执行,用于协调多线程的执行顺序。
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread task = new Thread(() -> {
// 任务逻辑
});
task.start();
task.join(); // 主线程等待task执行完成
}
}
3.4 线程中断机制
Java 采用协作式中断机制,通过设置中断标记通知线程终止,而非强制抢占式终止。
- 自定义标记位:通过
volatile修饰的布尔变量作为退出标志,线程循环判断该标记决定是否终止。 - interrupt() 方法:调用线程对象的
interrupt()方法设置中断标记。若线程处于sleep/wait/join阻塞状态,会抛出InterruptedException并清除标记;若线程处于运行状态,仅设置标记,需通过Thread.currentThread().isInterrupted()主动检测。
4 线程状态与流转
线程的生命周期由Thread.State枚举定义,共包含六种状态。
4.1 六种状态定义
| 状态 | 定义 |
|---|---|
| NEW | 线程对象已创建,尚未调用 start() 方法 |
| RUNNABLE | 可运行状态,包含正在 CPU 执行与就绪等待调度两种子状态 |
| BLOCKED | 阻塞状态,线程等待获取 synchronized 锁 |
| WAITING | 无限等待状态,需由其他线程主动唤醒 |
| TIMED_WAITING | 限时等待状态,达到指定时长后自动唤醒 |
| TERMINATED | 终止状态,线程执行逻辑已完成 |
4.2 状态流转逻辑
- 调用
start()后,线程从 NEW 进入 RUNNABLE 状态。 - 线程尝试获取锁失败时,进入 BLOCKED 状态;获取锁后回到 RUNNABLE。
- 调用
wait()、无超时join()时,线程释放锁并进入 WAITING 状态;被唤醒后重新竞争锁,成功后回到 RUNNABLE。 - 调用
sleep()、带超时wait()/join()时,线程进入 TIMED_WAITING 状态;超时或被唤醒后回到 RUNNABLE。 - 线程执行完毕后进入 TERMINATED 状态。
5 线程安全问题分析
5.1 线程安全的定义
在多线程并发执行环境下,程序运行结果与单线程串行执行的预期结果一致,则称该程序是线程安全的。
5.2 线程不安全的核心成因
(1)原子性缺失
原子性指一段操作要么完整执行,要么完全不执行,执行过程不会被其他线程打断。
Java 中看似原子的代码(如count++)实际包含“读内存-计算-写回内存”三步操作。多线程抢占式调度下,操作中途可能被切换,导致共享数据修改异常。
(2)内存可见性问题
Java 内存模型(JMM)规定:所有共享变量存储于主内存,每个线程拥有独立的工作内存(对应 CPU 寄存器与高速缓存)。线程操作共享变量时,先从主内存拷贝副本到工作内存,修改后再同步回主内存。
由于线程工作内存的存在,一个线程对共享变量的修改可能无法立即被其他线程感知,导致数据不一致。
(3)指令重排序
编译器与 CPU 为优化性能,会在不改变单线程语义的前提下调整指令执行顺序。在多线程场景下,指令重排序可能导致跨线程逻辑不一致。
6 线程安全的基础解决方案
6.1 synchronized 关键字
synchronized 即监视器锁,是 Java 内置的互斥同步机制,能够保证原子性、内存可见性,并禁止部分指令重排序。
核心特性
- 互斥性:同一时刻仅一个线程能获取指定对象的锁,进入同步代码块;其他尝试获取同一锁的线程会阻塞等待。锁状态存储于 Java 对象头中。
- 可重入性:同一线程可重复获取同一把锁,不会出现自身死锁。锁内部维护持有者线程与计数器,每加锁一次计数器递增,解锁时递减,计数器归零时锁真正释放。
三种使用形式
public class SyncDemo {
private final Object locker = new Object();
// 1. 修饰代码块,指定锁对象
public void method1() {
synchronized (locker) {
// 同步逻辑
}
}
// 2. 修饰普通方法,锁对象为当前实例this
public synchronized void method2() {
// 同步逻辑
}
// 3. 修饰静态方法,锁对象为当前类的Class对象
public static synchronized void method3() {
// 同步逻辑
}
}
标准库中的线程安全类
- 线程不安全类:
ArrayList、HashMap、HashSet、StringBuilder - 线程安全类:
StringBuffer、Vector、Hashtable;高并发场景推荐使用ConcurrentHashMap
6.2 volatile 关键字
volatile是轻量级同步机制,仅作用于变量,能够保证内存可见性与禁止指令重排序,但不保证原子性。
作用机制
- 写 volatile 变量时,强制将工作内存中的修改立即刷新回主内存。
- 读 volatile 变量时,强制从主内存读取最新值,绕过线程工作内存缓存。
适用场景与局限性
适用于一写多读的场景,如状态标记位。不适用于 count++ 等需要原子性的复合操作,此类场景仍需通过锁机制保证线程安全。
7 线程间协作:wait/notify 机制
wait()、notify()、notifyAll() 是 Object 类的原生方法,用于实现线程间的等待-唤醒协作,必须在同步代码块中以锁对象调用。
7.1 核心方法语义
| 方法 | 语义 |
|---|---|
wait() | 当前线程释放锁,进入对象的等待队列,无限期等待唤醒 |
wait(long timeout) | 限时等待,超时后自动唤醒 |
notify() | 随机唤醒等待队列中的一个线程;调用后不会立即释放锁,退出同步块后锁才释放 |
notifyAll() | 唤醒等待队列中所有线程,唤醒后所有线程重新竞争锁 |
需注意:wait() 应置于循环中调用,避免虚假唤醒导致逻辑异常。
7.2 wait 与 sleep 的差异
| 对比维度 | wait() | sleep() |
|---|---|---|
| 所属类 | Object类方法 | Thread类静态方法 |
| 锁要求 | 必须在同步块中调用 | 无需持有锁 |
| 锁行为 | 调用后释放持有的锁 | 调用后不释放锁 |
| 用途 | 线程间协作通信 | 单纯使线程暂停执行 |
8 多线程经典应用场景
8.1 单例模式
单例模式保证一个类在程序运行期间仅存在一个实例。
饿汉式实现
类加载时即创建实例,实现简单且天然线程安全;缺点是类加载即初始化,可能造成资源浪费。
class SingletonHungry {
private static final SingletonHungry INSTANCE = new SingletonHungry();
private SingletonHungry() {}
public static SingletonHungry getInstance() {
return INSTANCE;
}
}
双重校验锁懒汉式实现
首次调用时才创建实例,通过双重 if 判断降低锁竞争,结合 volatile 禁止指令重排序,兼顾线程安全与性能。
class SingletonLazy {
private static volatile SingletonLazy instance = null;
private SingletonLazy() {}
public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
8.2 阻塞队列与生产者消费者模型
阻塞队列是线程安全的队列结构,遵循先进先出原则:队列满时,入队操作阻塞;队列空时,出队操作阻塞。
阻塞队列是生产者消费者模型的核心实现组件,通过队列解耦生产者与消费者,平衡两者处理能力,实现削峰填谷。
Java 标准库提供 BlockingQueue 接口,常用实现为 LinkedBlockingQueue,核心方法为 put()(阻塞入队)与 take()(阻塞出队)。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ProducerConsumer {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
Thread producer = new Thread(() -> {
while (true) {
int num = (int) (Math.random() * 1000);
try {
queue.put(num);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者");
Thread consumer = new Thread(() -> {
while (true) {
try {
queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者");
producer.start();
consumer.start();
}
}
8.3 线程池
核心价值
频繁创建与销毁线程会产生较高开销。线程池通过复用一组已创建的线程执行任务,降低线程生命周期开销,提升系统响应速度。
Executors工具类
Executors提供线程池快速创建方法:
newFixedThreadPool(int n):创建固定线程数的线程池newCachedThreadPool():创建线程数动态增长的线程池newSingleThreadExecutor():创建单线程线程池newScheduledThreadPool(int n):创建支持定时任务的线程池
ThreadPoolExecutor核心参数
生产环境推荐手动创建ThreadPoolExecutor,自定义核心参数:
corePoolSize:核心线程数,常驻线程数量maximumPoolSize:最大线程数,核心线程与临时线程总数上限keepAliveTime:临时线程空闲存活时长workQueue:存放待执行任务的阻塞队列threadFactory:线程创建工厂RejectedExecutionHandler:任务拒绝策略,包含抛出异常、调用者执行、丢弃最旧任务、丢弃新任务四种实现。
9 线程安全设计思路
- 无共享设计:线程使用私有局部变量,从根源避免数据竞争。
- 只读设计:共享变量仅进行读操作,天然线程安全。
- 同步控制:
- 复合操作通过
synchronized或显式锁保证原子性; - 状态标记通过
volatile保证内存可见性; - 优先使用JDK内置并发工具类,避免手动实现复杂同步逻辑。
- 复合操作通过
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)