
java基础-多线程
一、程序、进程、线程、并发并行
-
程序:磁盘上静态代码文件,无运行资源
-
进程:操作系统资源分配最小单位,独立内存 / CPU 资源,进程间隔离,开销大
-
线程:进程内执行单元,CPU 调度最小单位,共享进程堆 / 方法区,私有栈、程序计数器
-
并发:单核 CPU 时间片轮转,宏观同时执行,微观串行
-
并行:多核 CPU,同一时刻多个线程真正同时运行
线程调度机制
-
时间片轮转:同优先级线程均分 CPU 时间
-
抢占式:高优先级线程可抢夺 CPU,Java 线程调度由 OS 决定,优先级仅为建议,不保证绝对生效
线程优先级
常量:MIN_PRIORITY=1、NORM_PRIORITY=5(默认)、MAX_PRIORITY=10
方法:setPriority() / getPriority()
注意:优先级只是操作系统参考,不能百分百控制执行顺序,不要依赖优先级做业务逻辑
用户线程 & 守护线程
-
用户线程(前台线程):业务主线程,JVM 必须等所有用户线程执行完毕才退出
-
守护线程(后台线程):依附用户线程,所有用户线程结束,JVM 直接退出,不会等待守护线程执行完
-
设置:
thread.setDaemon(true),必须在start()前调用 -
典型:GC 垃圾回收线程、定时后台心跳线程
-
-
Java 默认至少 2 个线程:main 主线程 + GC 守护线程
二、四种线程创建方式
方式 1:继承 Thread 类
class MyThread extends Thread{
@Override
public void run() {
System.out.println("子线程执行");
}
}
//调用
new MyThread().start();
缺点:单继承限制,无法再继承其他类,耦合高
方式 2:实现 Runnable 接口(推荐)
无返回、无异常,解决单继承问题,资源共享友好
class MyRun implements Runnable{
@Override
public void run() {
System.out.println("runnable线程");
}
}
//调用
Thread t = new Thread(new MyRun());
t.start();
方式 3:Callable + FutureTask(有返回、可抛异常)
Runnable 无返回,Callable 弥补该缺陷,底层依然适配 Thread
class MyCall implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 100;
}
}
//使用
FutureTask<Integer> task = new FutureTask<>(new MyCall());
new Thread(task).start();
Integer res = task.get(); //阻塞获取返回结果
方式 4:线程池(见下文)
三、线程状态
Thread.State枚举定义 6 种,无第七种:
-
NEW 新建:new Thread 后,未调用 start
-
RUNNABLE 可运行:包含就绪 + 运行两种状态,等待 CPU / 正在执行
-
BLOCKED 阻塞:等待 synchronized 监视器锁
-
WAITING 无限等待:
wait()/join(),无超时,需要其他线程唤醒 -
TIMED_WAITING 限时等待:
sleep(ms)/wait(ms)/join(ms),超时自动唤醒 -
TERMINATED 终止:run 执行完毕 / 异常结束,线程死亡
常用线程 API 修正误区
-
start():创建并启动新线程,底层调用 native 方法,自动执行 run;多次调用抛异常 -
run():普通方法,直接调用不会新开线程 -
sleep(long):限时等待,不释放锁,时间到进入 RUNNABLE -
yield():线程礼让,让出 CPU 回到就绪,不释放锁,礼让不保证生效 -
join():当前线程阻塞,等待目标线程执行完毕,底层调用 wait,会释放锁 -
interrupt():仅标记中断状态,不会直接停止线程;-
线程 sleep/wait/join 阻塞时,抛出
InterruptedException,并清除中断标记 -
正常运行线程:
isInterrupted()返回 true,业务自行判断终止
-
如何安全停止线程
-
弃用:
stop()/suspend()/resume(),强制终止,锁不释放,数据错乱 -
方案 1:自定义 volatile 布尔标记(简单业务)
-
方案 2:interrupt 中断标记(推荐,适配阻塞场景)
public class StopThreadTest {
private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (flag && !Thread.currentThread().isInterrupted()){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); //重置标记
break;
}
System.out.println("运行中");
}
});
t.start();
Thread.sleep(3000);
flag = false; //方式1
//t.interrupt(); //方式2
}
}
四、synchronized 同步锁
锁对象区分
-
实例同步方法:锁当前对象
this,不同实例互不阻塞 -
静态同步方法:锁类字节码对象
类.class,全局唯一,所有实例互斥 -
同步代码块:手动指定锁对象(this / 自定义对象 / 类.class)
持有锁时,什么场景会释放监视器锁
✅ 释放锁:
-
同步代码 / 方法正常执行完毕
-
遇到 return、break 跳出同步代码
-
代码抛出异常,JVM 自动释放锁
-
同步块内调用
wait(),释放锁进入等待队列
❌ 不会释放锁:
-
sleep()、yield()仅让出 CPU,持有锁不放 -
调用
suspend()(废弃)挂起线程,锁持续持有
synchronized
属于悲观锁、独占锁、可重入、非公平锁,JVM 自动管理锁;存在锁升级:偏向锁→轻量级锁→重量级锁
更多底层见JUC篇章
五、锁分类
1. 悲观锁 vs 乐观锁
-
悲观锁:默认并发修改冲突,操作直接加锁,synchronized、ReentrantLock
-
乐观锁:无锁,更新校验版本,冲突重试,CAS、版本号控制(JUC 详述 CAS)
2. 独占锁 vs 共享锁
-
独占:同一时间仅一线程持有,synchronized、ReentrantLock
-
共享:多线程同时持有读锁,ReentrantReadWriteLock(读写锁,JUC)
3. 可重入锁(递归锁)
同一线程多次获取同一把锁不会死锁,synchronized、ReentrantLock 都支持
场景:同步方法 A 调用同类同步方法 B,无需重新竞争锁
4. 公平锁 / 非公平锁
-
公平:按线程排队顺序获取锁,吞吐量低,ReentrantLock 可构造开启
-
非公平:线程直接抢占锁,吞吐量高,synchronized 默认、Lock 默认
六、线程池
1. 使用线程池四大优势
-
复用线程,避免频繁创建销毁线程,降低性能开销
-
控制最大并发线程,防止无限创建线程导致 OOM
-
统一管理线程,可定时、延时、批量执行任务
-
支持拒绝策略,并发过载时友好处理任务
2. ThreadPoolExecutor 七大核心参数(原文保留,补充释义)
-
corePoolSize:核心线程数,常驻池,默认空闲不回收
-
maximumPoolSize:最大总线程数 = 核心 + 临时非核心线程
-
keepAliveTime:非核心线程空闲超时时间,超时回收
-
TimeUnit:时间单位
-
workQueue:阻塞队列,核心线程满后任务存入
-
ArrayBlockingQueue:有界队列,固定长度
-
LinkedBlockingQueue:无界队列,堆积任务 OOM 风险
-
SynchronousQueue:无存储,任务直接交付线程,无线程则新建
-
DelayQueue:延时任务专用
-
-
ThreadFactory:线程工厂,自定义线程名、守护属性,方便日志排查
-
RejectedExecutionHandler:拒绝策略,队列 + 线程全部打满时执行
四种内置拒绝策略
-
AbortPolicy(默认):直接抛出 RejectedExecutionException
-
CallerRunsPolicy:交给提交任务的主线程执行,减缓提交速度
-
DiscardOldestPolicy:丢弃队列队首最老任务,重新提交当前任务
-
DiscardPolicy:静默丢弃当前新任务,无报错
3. 四种 Executors 工具类线程池 & 禁止使用原因
-
newFixedThreadPool:固定核心线程,无界队列,任务无限堆积 OOM
-
newSingleThreadExecutor:单线程串行执行,无界队列
-
newCachedThreadPool:核心 0,最大 Integer.MAX_VALUE,瞬时大量任务创建无限线程 OOM
-
newScheduledThreadPool:定时任务池,存在无界队列隐患
规范:生产禁止 Executors 快速创建,手动 new ThreadPoolExecutor 自定义参数
4. 线程池任务执行完整流程
-
execute 提交 Runnable 任务
-
当前运行线程数 < corePoolSize → 新建核心线程执行
-
核心线程已满 → 尝试放入阻塞队列
-
队列已满 → 判断总线程 < maximumPoolSize → 创建临时非核心线程
-
总线程达到最大值 → 触发拒绝策略
5. 线程池五大运行状态
-
RUNNING:正常运行,接收新任务、处理队列任务
-
SHUTDOWN:调用 shutdown (),停止接收新任务,执行完队列剩余任务
-
STOP:调用 shutdownNow (),停止新任务,中断正在执行任务,清空队列
-
TIDYING:所有任务结束,线程数为 0,即将执行 terminated () 钩子
-
TERMINATED:钩子方法执行完成,线程池彻底关闭
6. 线程池两种提交方式
-
execute(Runnable):无返回,异常直接抛出 -
submit():支持 Runnable/Callable,返回 Future,可捕获线程内异常
七、ThreadLocal
-
作用:线程本地变量隔离,每个线程独有一份数据,线程间互不干扰
-
底层:每个 Thread 持有
ThreadLocalMap,key 为弱引用 ThreadLocal 对象,value 为存储数据 -
常用场景:
-
多线程存储登录用户上下文
-
数据库连接、事务绑定当前线程
-
-
内存泄漏问题:
-
key 弱引用 GC 自动回收,但 value 强引用不会释放
-
规范使用:线程使用完毕手动调用
remove()清除数据,尤其线程池复用线程场景
-
八、synchronized 和 ReentrantLock 对比
| 对比维度 | synchronized | ReentrantLock(JUC) |
|---|---|---|
| 语法 | Java 关键字,隐式锁 | API 类,手动 lock ()/unlock (),必须 finally 释放 |
| 锁实现 | JVM 底层监视器锁 | AQS 同步器实现 |
| 公平锁 | 仅非公平,不可修改 | 支持公平 / 非公平,构造传参 |
| 可中断 | 阻塞等待不可中断 | lockInterruptibly () 可中断等待 |
| 条件变量 | 不支持 | 多个 Condition 分组等待唤醒 |
| 锁升级 | 偏向 / 轻量 / 重量级锁优化 | 无锁升级机制 |
| 可重入 | 支持 | 支持 |
| 性能 | JDK1.6 优化后差距极小 | 高并发场景灵活可控 |
基础题
-
并发和并行区别?
-
start () 和 run () 区别,为什么不能直接调用 run 开启线程?
-
线程有几种状态,BLOCKED 和 WAITING 区别?
-
sleep 和 wait 区别?
答:sleep 是 Thread 静态方法,不释放锁,指定时间自动唤醒;wait 是 Object 方法,释放锁,需 notify/notifyAll 唤醒,仅同步代码内使用。
-
守护线程和用户线程区别,GC 是什么线程?
锁与同步
-
synchronized 同步方法锁对象分别是什么?
-
什么是可重入锁,作用?
-
乐观锁悲观锁区别,各自适用场景?
-
synchronized 什么时候释放锁,sleep 会释放锁吗?
线程池高频
-
线程池七大参数分别作用?
-
四种拒绝策略介绍,业务如何选择?
-
为什么禁止使用 Executors 创建线程池?
-
线程池 execute 提交任务完整流程?
-
shutdown () 和 shutdownNow () 区别?
ThreadLocal
-
ThreadLocal 原理,为什么线程间数据隔离?
-
ThreadLocal 内存泄漏原因,如何避免?
综合问答
-
停止线程有几种方式,为什么弃用 stop?
-
synchronized 和 ReentrantLock 区别?
-
简述四种创建线程方式,各自优缺点?
-
线程池好处,生产如何自定义线程池?