目录
多线程
1.进程与线程
想象一下,开了一家餐厅(服务器)
进程(Process)=整个餐厅,如果餐厅倒闭了=进程结束了,里面的东西都没了
线程(Thread)=厨师。单线程就是一个厨师干店里面活,多线程就是多个厨师干店里面的活
2.并发与并行
并发:同一时间内,A先发生,B后发生(多个任务在同一个内核上)
并行:同意时间内,AB同时发生(多个任务在多个内核上)
3.线程的使用创建
(1)继承 Thread 类 (不推荐,因为受限于 Java 的单继承特性(继承了 Thread 就不能继承其他业务类了)
// 1. 定义一个类继承 Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("子线程运行中... 当前线程: " + Thread.currentThread().getName());
// 写你的业务逻辑
}
}
// 2. 创建对象并启动
public class Demo1 {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // 必须调用 start(),不能直接调用 run()
// 如果调用 t.run(),那就只是普通方法调用,还是在主线程执行!
System.out.println("主线程继续运行...");
}
}
(2)实现 Runnable 接口 (推荐,将“任务逻辑”和“线程控制”分离,避免了单继承的限制)
// 1. 定义任务类实现 Runnable
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("任务执行中... 当前线程: " + Thread.currentThread().getName());
}
}
public class Demo2 {
public static void main(String[] args) {
MyTask task = new MyTask();
// 2. 把任务交给 Thread 对象
Thread t = new Thread(task);
t.start();
// Lambda 简化写法 (Java 8+)
new Thread(() -> System.out.println("Lambda 方式运行")).start();
}
}
(3)实现 Callable 接口 + FutureTask (需要返回值时用),如果你需要线程执行完后返回结果,或者需要抛出检查型异常,用这个
import java.util.concurrent.*;
public class Demo3 {
public static void main(String[] args) throws Exception {
// 1. 定义任务,可以有返回值 (Integer)
Callable<Integer> task = () -> {
System.out.println("计算中...");
Thread.sleep(2000); // 模拟耗时
return 100 + 200; // 返回结果
};
// 2. 包装成 FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(task);
Thread t = new Thread(futureTask);
t.start();
System.out.println("主线程在做别的事...");
// 3. 获取结果 (会阻塞,直到子线程算完)
// 如果子线程没跑完,这里会一直等;跑完了直接拿结果
Integer result = futureTask.get();
System.out.println("子线程返回结果: " + result);
}
}
(4)使用线程池
4.线程的生命周期
| NEW | 新建 |
| RUNNABLE | 可运行(正在运行或等待CPU) |
| BLOCKED | 阻塞,等待锁 |
| WAITING | 等待(无超时,等待其他线程 |
| TIMED_WAITING | 超时等待(如 sleep、wait(long)) |
| TERMINATED | 终止 |
5.常用方法
| 方法名 | 作用 |
start() | 启动线程。JVM 会分配资源并调用 run() 方法。 |
run() | 线程的具体执行逻辑(任务代码)。 |
join() | 等待线程终止。调用 t.join() 的线程会阻塞,直到 t 执行完。 |
join(long ms) | 等待线程终止,但最多等指定毫秒数。 |
interrupt() | 中断线程。给线程设置一个“中断标志位”。 |
isInterrupted() | 检查线程是否被中断(返回 boolean)。 |
Thread.interrupted() | (静态方法) 检查当前线程是否中断,并清除标志位。 |
setDaemon(true) | 设置为守护线程。 |
ThreadLocal
定义:让每个线程拥有自己独立的变量副本,互不干扰,用于线程隔离
1.常用方法
| 方法 | 签名 | 作用 | 返回值/行为 |
|---|---|---|---|
set(T value) | void set(T value) | 存值。将值绑定到当前线程。 | 无返回值。后续该线程调用 get() 将返回此值。 |
get() | T get() | 取值。获取当前线程绑定的值。 | 如果从未 set 过,默认返回 null(除非重写了 initialValue)。 |
remove() | void remove() | 删值。移除当前线程绑定的值。 | 极其重要:防止内存泄漏,必须在 finally 块中调用。 |
initialValue() | protected T initialValue() | 初始值。当第一次 get() 且未 set 时调用。 | 通常通过匿名内部类或 Lambda 重写,提供默认值(如 new ArrayList())。 |
2.使用步骤(以苍穹外卖为列)
(1)定义 ThreadLocal 变量
(2)设置值 (Set)。在请求进入时(如拦截器、Filter、AOP)或任务开始时,将数据存入。
(3)获取值 (Get)。在业务逻辑的任何地方(Service, Controller, Mapper),只要是在同一个线程内,都可以直接取,不需要层层传递参数。
(4)清理值 (Remove) 这是最关键的一步! 必须在请求结束或任务完成后清理,否则会导致内存泄漏。通常在 finally 块中执行。
package sky.context;
public class BaseContext {
/**
* 基于ThreadLocal封装工具类,用于保存和获取当前登录用户ID
*/
//定义 ThreadLocal 变量
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
//设置当前线程的ID
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
//获取当前线程的ID
public static Long getCurrentId() {
return threadLocal.get();
}//移除当前线程的ID
public static void removeCurrentId() {
threadLocal.remove();
}
}
3.ThreadLocal与Synchronized的区别
| 特性 | synchronized (锁机制) | ThreadLocal (线程封闭) |
|---|---|---|
| 核心思想 | 以时间换空间 通过牺牲时间(等待锁),让多个线程共享同一个变量。 | 以空间换时间 通过牺牲内存(每个线程存一份副本),让线程之间互不干扰。 |
| 数据共享性 | 共享:所有线程操作的是同一个变量。 | 隔离:每个线程操作的是自己独有的变量副本。 |
| 并发方式 | 串行:同一时刻,只有一个线程能执行临界区代码(排队)。 | 并行:所有线程同时执行,互不阻塞,无需等待。 |
| 适用场景 | 需要线程间通信、数据必须一致的场景(如:计数器、库存扣减)。 | 数据无需共享、仅需在当前线程内传递的场景(如:用户上下文、数据库连接、事务管理)。 |
| 性能开销 | 高:涉及线程挂起、唤醒、上下文切换,竞争激烈时性能下降明显。 | 低:无锁竞争,直接读写本地内存,速度极快(但消耗更多内存)。 |
| 主要风险 | 死锁 (Deadlock): 线程 A 等 B 释放锁,B 等 A 释放锁,大家永远卡住。 | 内存泄漏 (Memory Leak): 线程池复用线程时,如果不手动 remove(),旧数据会一直占着内存,甚至导致下一个请求读到脏数据(串号)。 |
| 性能瓶颈 | 锁竞争: 线程多了,大家都在排队,CPU 大量时间花在切换线程上。 | 内存占用: 线程越多,占用的内存越多。如果存的对象很大,容易 OOM。 |
Synchronized
1.使用方式
(1)修饰实例方法(锁的是 this)
public synchronized void doSomething() {
// 临界区代码
}
(2)修饰静态方法(锁的是 Class 对象)
public static synchronized void doStaticSomething() {
// 临界区代码
}
(3) 修饰代码块(锁的是指定的对象)最推荐
public void doWork() {
// 这里的 "lockObj" 可以是 this, 也可以是 new Object(), 或者一个常量
synchronized (this) {
// 临界区代码
}
}
锁
在多线程编程中,锁(Lock) 是解决线程安全问题的核心机制。
它的本质是:同一时刻,只允许一个线程访问共享资源。如果没有锁,多个线程同时修改同一个变量(如 count++),就会导致数据错乱(线程不安全)。
1.死锁
死锁 (Deadlock) 是多线程编程中最棘手的问题之一。简单来说,就是两个或多个线程互相等待对方释放资源,结果谁也无法继续执行,大家永远卡在那里。
(1)产生死锁的 4 个必要条件
| 名称 | 解释 |
|---|---|
| 互斥条件 (Mutual Exclusion) | 资源是独占的。同一时刻,一个资源只能被一个线程占用。(如:筷子不能劈成两半用) |
| 请求与保持 (Hold and Wait) | 线程已经持有了至少一个资源,但又去请求新的资源,而新资源被别的线程占用了,此时当前线程阻塞,但不释放已持有的资源。 |
| 不剥夺条件 (No Preemption) | 线程已获得的资源,在未使用完之前,不能被其他线程强行夺走,只能由自己主动释放。 |
| 循环等待 (Circular Wait) | 存在一种线程资源的环形链:T1 等 T2,T2 等 T3 ... Tn 等 T1。 |
(2)代码示例
public class DeadLockDemo {
// 定义两个锁对象
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
// 线程 1:先拿 A,再拿 B
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("线程 1 获得了锁 A,准备获取锁 B...");
try { Thread.sleep(100); } catch (InterruptedException e) {} // 模拟耗时,增加死锁概率
synchronized (lockB) {
System.out.println("线程 1 获得了锁 B");
}
}
});
// 线程 2:先拿 B,再拿 A (顺序反了!)
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("线程 2 获得了锁 B,准备获取锁 A...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockA) {
System.out.println("线程 2 获得了锁 A");
}
}
});
t1.start();
t2.start();
// 程序将在这里卡住,没有任何输出后续...
}
}
2.乐观锁与悲观锁
核心区别就一句话:你是“先锁门再办事”,还是“先办事再检查”?
| 维度 | 悲观锁 (Pessimistic) | 乐观锁 (Optimistic) | 通俗大白话解释 |
|---|---|---|---|
| 假设前提 | 假设冲突一定会发生 | 假设冲突很少发生 | 悲观:“人心险恶,肯定有人抢!” 乐观:“大家都有素质,应该没人抢。” |
| 加锁时机 | 操作前就加锁 | 更新提交时才检查 | 悲观:进屋前先把门反锁。 乐观:进屋不锁门,出门前看一眼有没有人动过东西。 |
| 实现机制 | 依赖独占锁 (如: synchronized) | 依赖版本号或对比 (如:CAS) | 悲观:拿一把唯一的钥匙,谁有钥匙谁进。 乐观:给文件贴个标签(版本号),改的时候看标签变没变。 |
| 线程行为 | 获取不到?→ 阻塞 (睡觉) 乖乖排队等待 | 获取失败?→ 不阻塞 要么放弃,要么重试 | 悲观:门口排队,原地罚站,直到轮到你了才醒。 乐观:发现被人抢先了?不排队,要么回家,要么重新再试一次。 |
| CPU 开销 | 低 (睡觉不费电) | 高 (一直重试很费电) | 悲观:排队的人都在睡觉,不消耗体力 (CPU)。 乐观:失败的人一直在原地转圈重试,累得半死 (浪费 CPU)。 |
| 吞吐量 | 低 (一个个来,慢) | 高 (大家一起跑,快) | 悲观:像过独木桥,一次只能过一个人,后面堵长队。 乐观:像宽阔马路,大家并排跑,只有撞车了才停下来处理。 |
| 典型场景 | 写入频繁 (如:抢票、转账) | 读取频繁 (如:看新闻、查余额) | 悲观:大家都要改数据(如抢红包),必须排队防乱。 乐观:大部分人只是看数据,偶尔改一下,不用排队。 |
| Java 代表 | synchronizedReentrantLock | AtomicIntegerLongAdder | 悲观:Java 里的“重锁”工具。 乐观:Java 里的“原子类”工具 (自带重试功能)。 |
| 数据库代表 | SELECT ... FOR UPDATE | UPDATE ... WHERE version=旧值 | 悲观:查出来直接锁住行,别人动不了。 乐观:更新时带个条件:“只有版本号没变我才改”,变了就不改。 |
436

被折叠的 条评论
为什么被折叠?



