1.并行与并发
并发:多个任务在同一个CPU上,按照细分的时间片轮流交替执行,由于时间很短,看上去好像是同时进行的。
并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的同时进行。
2.线程的状态
Java线程通常有五种状态,分别是创建、就绪、运行、阻塞、死亡。
- 新建状态(New) :当程序使用
new关键字创建了一个线程后,该线程就处于新建状态,此时线程还未启动。当线程对象调用start()方法时,线程启动,进入Runnable状态。 - 可运行(就绪)状态(Runnable) :在Java语言中,就绪状态和运行状态被合并成RUNNABLE状态。当线程处于Runnable状态时,表示线程准备就绪,等待获取CPU。线程对象调用
start()方法后,线程进入就绪状态,等待被线程调度选中,获取CPU的使用权。 - 运行(正在运行)状态(Running) :若该线程获取了CPU,则进入Running状态,开始执行线程体,即
run()方法中的内容。如果系统只有1个CPU,那么在任意时间点则只有1条线程处于Running状态;如果是双核系统,那么同一时间点会有2条线程处于Running状态。但是,当线程数大于处理器数时,依然会是多条线程在同一个CPU上轮换执行。当一条线程开始运行时,若它不是一瞬间完成,那么它不可能一直处于Running状态,线程在执行过程中会被中断,目的是让其它线程获得执行的机会。调用yield()方法,可以使线程由Running状态进入Runnable状态。 - 阻塞(挂起)状态(Block) :阻塞又分为三种情况。等待阻塞:运行的线程执行
wait方法,则该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中,进入这个状态后是不能被自动唤醒的,需要调用notify/notifyAll方法才能被唤醒。同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其它线程占用,则JVM会把该线程放入“锁池”中。其它阻塞:运行的线程执行sleep/join方法、或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep方法等待超时、join方法等待线程终止或者超时、I/O处理完毕时,线程将重新转入就绪状态。当线程进入阻塞状态,阻塞结束时,该线程将进入Runnable状态,而非直接进入Running状态。 - 死亡状态(Dead) :当线程的
run()方法执行结束,线程进入Dead状态。不要试图对一个已经死亡的线程调用start()方法,线程死亡后将不能再次作为线程执行,系统会抛出IllegalThreadStateException异常。
状态转换情况如下:
- 当一个线程使用
start方法时,就会从NEW状态->RUNNABLE状态。 - 当一个线程运行完
run方法时,就会从RUNNABLE状态->TERMINATED状态。 - 当一个线程因为不带参数的
join()或者wait()阻塞等待的时候,此时等待的那个线程运行结束或者wait被notify唤醒时,就会从WAITING状态->RUNNABLE状态或者TERMINATED状态。 - 当一个线程因为带参数
join(1000)或者wait(1000)阻塞等待的时候,此时等待的时间到达时,就会从TIMED_WAITING状态->RUNNABLE状态或者TERMINATED状态。 - 当一个线程由于锁竞争而导致阻塞时,此时当这个线程获得锁之后,就会从BLOCKED状态->RUNNABLE状态
3.线程创建方式
1.通过继承Thread类,并重写其run()方法,在run()方法中定义线程要执行的任务
2.实现Runnable接口,实现其run()方法,将实现类的实例作为参数传递给Thread类的构造函数来创建线程
3.实现Callable接口,实现其call()方法,该方法可以有返回值。需要使用FutureTask类来包装Callable对象,再将FutureTask对象作为参数传递给Thread类的构造函数来创建线程
4.对于需要频繁创建和销毁线程的场景,使用线程池是更好的选择。可以通过Executors工具类创建不同类型的线程池,也可以使用ThreadPoolExecutor自定义线程池
4.start()与run()的区别
| 特性 | start()方法 | run()方法 |
|---|---|---|
| 线程创建 | ✅ 创建新线程 | ❌ 不创建新线程 |
| 执行位置 | 新线程中执行run() | 当前线程中直接执行 |
| 调用效果 | 异步执行(多线程) | 同步执行(单线程) |
| JVM角色 | 请求JVM创建新线程 | 普通方法调用 |
| 多次调用 | ⚠️ 同一线程对象只能调用1次(否则抛IllegalThreadStateException) | ✅ 可多次调用 |
5.wait()与sleep()方法的区别
-
来源不同:
wait()是Object类的方法。sleep()是Thread类的静态方法。
-
锁的行为:
wait()方法调用后,当前线程会释放它所持有的对象锁(monitor)。sleep()方法调用后,当前线程不会释放任何锁,仍然持有锁。
-
使用条件:
wait()必须在同步块(synchronized方法或同步代码块)中使用,否则会抛出IllegalMonitorStateException。sleep()可以在任何地方使用,不需要在同步块中。
-
唤醒机制:
wait()可以被其他线程通过调用同一个对象的notify()或notifyAll()方法来唤醒。sleep()在指定的时间过后会自动唤醒,或者可以通过调用该线程的interrupt()方法来中断休眠,此时会抛出InterruptedException。
-
线程状态:
wait()会让线程进入等待状态(WAITING或TIMED_WAITING,如果指定了超时时间),直到被唤醒。sleep()会让线程进入睡眠状态(TIMED_WAITING),直到时间到期或被中断。
-
用途:
wait()通常用于线程间通信,等待某个条件满足。sleep()通常用于暂停执行一段时间。
6.notify()与notifyAll()区别
-
notify()和notifyAll()都是Object类的方法,用于唤醒在对象上等待的线程。
-
区别:
- notify():随机唤醒一个在该对象上等待的线程(具体哪个线程取决于JVM实现),被唤醒的线程将从等待队列(WAITING)移到同步队列(BLOCKED),并尝试获取对象锁。
- notifyAll():唤醒所有在该对象上等待的线程,所有被唤醒的线程都将从等待队列移到同步队列,然后竞争对象锁。
-
注意事项:
- 使用notify()时,如果多个线程在等待,而只唤醒其中一个,那么其他线程可能会一直等待下去(直到再次被通知),这可能导致“线程饥饿”。
- 使用notifyAll()会唤醒所有等待线程,但只有一个线程能获得锁,其余线程会继续阻塞在同步队列中,这可能会造成一定的性能开销(因为很多线程被唤醒但只能有一个执行,其余又会被阻塞)。
-
使用场景:
- 当所有等待线程都是同质的(即它们等待的条件相同,任何一个被唤醒都能执行)时,使用notify()效率更高。
- 当等待线程是异质的(它们等待的条件可能不同,需要唤醒所有线程来检查各自的条件)时,必须使用notifyAll(),以避免有线程永远不被唤醒。
-
最佳实践:
- 通常,我们建议使用notifyAll(),因为它更安全,可以避免有线程被遗漏唤醒。但是,在明确知道只有一个线程需要被唤醒,或者所有等待线程都是同质的情况下,可以使用notify()以提升性能。
7.线程出现安全问题的判断依据
判断依据:
当同时满足以下三个条件时,即可判断存在线程安全问题:
- 多线程环境
- 共享资源访问
- 至少一个线程执行写操作
- 无适当同步机制保障(原子性、可见性、有序性)
💡 最佳实践:
- 使用
synchronized或java.util.concurrent.locks包下的锁机制保证原子性。- 使用
volatile关键字或锁保证可见性。- 避免指令重排序(如使用
final修饰不可变对象,或利用volatile禁止重排序)
8.虚拟线程有哪些好处
- 资源高效:虚拟线程消耗的内存和CPU资源远少于平台线程(传统线程),使得创建大量线程成为可能(甚至数百万个)。
- 高并发:通过将大量虚拟线程映射到少量操作系统线程,可以支持极高的并发量,特别适合处理大量IO操作(如网络请求、文件读写)。
- 简化编程模型:开发者可以像编写顺序代码一样编写并发代码,而不需要复杂的线程池管理和回调地狱。
- 减少上下文切换开销:虚拟线程的挂起和恢复由JVM管理,发生在用户空间,不涉及操作系统内核,因此切换代价极小。
- 避免常见并发问题:由于虚拟线程的设计,可以减少死锁、内存泄漏等问题,并且通过结构化并发(Structured Concurrency)可以更安全地管理线程生命周期
9.线程池创建的几个参数
- corePoolSize: 核心线程数,即线程池中保持存活的最小线程数量,即使这些线程处于空闲状态。
- maximumPoolSize: 最大线程数,线程池允许创建的最大线程数量。
- keepAliveTime: 非核心线程的空闲存活时间。当线程池中的线程数量超过corePoolSize时,多余的空闲线程在等待新任务的最长时间,超过这个时间它们将被终止。
- unit: keepAliveTime参数的时间单位(如TimeUnit.SECONDS)。
- workQueue: 任务队列,用于保存等待执行的任务的阻塞队列。常用的有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等。
- threadFactory: 线程工厂,用于创建新线程。可以自定义线程的名称、优先级等。
- handler: 拒绝策略。当线程池和任务队列都满了,无法处理新任务时,采取的拒绝策略。常用的有AbortPolicy(抛出异常)、CallerRunsPolicy(由调用线程执行该任务)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最旧的任务然后重试)。
| 参数名称 | 作用描述 | 典型设置示例 |
|---|---|---|
corePoolSize | 常驻核心线程数(即使空闲也不会回收) | CPU_core |
maximumPoolSize | 线程池最大扩容上限(当队列满时创建新线程) | CPU_core×2 |
keepAliveTime | 非核心线程空闲存活时间(超时自动回收) | 60秒 |
unit | 存活时间单位(秒/毫秒等) | TimeUnit.SECONDS |
workQueue | 任务等待队列(存放待执行任务) | ArrayBlockingQueue |
threadFactory | 线程创建工厂(自定义线程名称/优先级等) | DefaultThreadFactory |
handler | 拒绝策略(当线程池和队列都满时的处理方式) | AbortPolicy |
| 队列类型 | 特性 |
|---|---|
SynchronousQueue | 直接传递队列(不存储任务,需立即执行) |
LinkedBlockingQueue | 无界队列(可能导致OOM) |
ArrayBlockingQueue | 有界队列(需指定容量,如QUEUE_CAPACITY=200) |
| 策略类型 | 行为 |
|---|---|
AbortPolicy | 抛出RejectedExecutionException(默认) |
CallerRunsPolicy | 用调用者线程直接执行任务 |
DiscardPolicy | 静默丢弃新任务 |
DiscardOldestPolicy | 丢弃队列头部任务并重试 |
10.ThreadPool的参数配置参考
-
核心线程数设置依据:
- CPU核心数:通过Runtime.getRuntime().availableProcessors()获取,记为NN
- 任务类型:
• CPU密集型任务:线程数≈N+1
• IO密集型任务:线程数≈2N - 混合型任务: 线程数 ≈ CPU线程数 × (1 + I/O耗时 / CPU耗时)
-
等待时间(keepAliveTime)设置:
- 非核心线程空闲超过此时间将被回收
- 设置原则:
• 频繁波动场景:设置较短时间(如30-60秒)避免资源浪费
• 稳定负载场景:可适当延长(如几分钟)减少线程重建开销
• 特殊需求:若需维持线程池最小处理能力,可将corePoolSize设为0,全部线程使用keepAliveTime回收(需配合allowCoreThreadTimeOut参数)
-
注意事项:
- 最大线程数(maximumPoolSize)通常设置为核心线程数的2倍
- 队列容量需根据业务峰值合理设置,避免队列过大导致OOM或过小触发频繁扩容
具体实施步骤:
步骤1:确定任务类型
- 计算密集型:大部分时间在CPU运算(如科学计算)
- IO密集型:大部分时间在等待IO(如数据库查询、网络请求)
- 混合型:通过性能监控工具测量W(等待时间)和C(计算时间)比值
步骤2:计算核心线程数
-
示例1:4核服务器处理CPU密集型任务 → corePoolSize = 4 + 1 = 5
-
示例2:8核服务器处理IO密集型任务 → corePoolSize = 8 * 2 = 16
步骤3:设置等待时间
- 常规推荐值:30秒~5分钟
- 动态调整建议:通过监控线程空闲率调整
11.submit与execute方法的区别
| 特性 | execute() | submit() |
|---|---|---|
| 返回值 | 无返回值 (void) | 返回 Future 对象 |
| 任务类型 | 仅支持 Runnable | 支持 Runnable 和 Callable |
| 方法来源 | Executor 接口定义 | ExecutorService 接口扩展 |
12.ReentLock与sychronized区别
1.锁实现机制对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现原理 | JVM 内置监视器锁(Monitor) | 基于 AQS(AbstractQueuedSynchronizer)的 API 实现 |
| 锁类型 | 非公平锁(默认) | 支持公平锁和非公平锁(通过构造函数指定) |
| 锁获取方式 | 自动获取和释放 | 需显式调用 lock() 和 unlock() |
| 底层依赖 | JVM 原生支持 | JDK 层面的 API 实现 |
2.异常处理对比
| 场景 | synchronized | ReentrantLock |
|---|---|---|
| 同步块内异常 | 自动释放锁 | 需在 finally 中手动释放锁 |
| 死锁风险 | 较高(不可中断) | 较低(支持尝试获取锁) |
3.适用场景对比
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 简单同步逻辑 | synchronized | 简洁安全,自动管理锁 |
| 需要公平锁 | ReentrantLock | 构造函数指定公平策略 |
| 跨方法锁传递(锁链) | ReentrantLock | 可灵活控制锁边界 |
| 分组唤醒等待线程 | ReentrantLock+Condition | 多个条件队列支持 |
| 高并发竞争优化 | ReentrantLock | CAS 减少线程切换 |
13.悲观锁与乐观锁
悲观锁
对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。
乐观锁
乐观锁,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。一旦多个线程发生冲突,乐观锁通常使用一种称为 CAS 的技术来保证线程执行的安全性。
14.什么是CAS
在 CAS 中,有这样三个值:
- V:要更新的变量(var)
- E:预期值(expected)
- N:新值(new)
比较并交换的过程如下:
判断 V 是否等于 E,如果等于,将 V 的值设置为 N;如果不等,说明已经有其它线程更新了 V,于是当前线程放弃更新,什么都不做。
这里的预期值 E 本质上指的是“旧值”
15.什么是死锁,该如何避免
死锁是多进程/多线程系统中的一种僵持状态,当两个或多个进程在执行过程中,因争夺资源而陷入相互等待的阻塞状态。每个进程都持有部分资源,同时等待其他进程释放资源,若无外力干预,所有进程将无限期等待无法推进
- 预防策略(破坏必要条件)
| 目标 | 方法 | 实现示例 |
|---|---|---|
| 破坏占有并等待 | 一次性申请所有资源 | 事务开始时申请全部所需资源 |
| 破坏不可剥夺 | 允许强制回收资源 | 优先级高的进程可抢占资源 |
| 破坏循环等待 | 资源有序分配法 | 所有进程按固定顺序请求资源 |
- 动态避免策略
-
银行家算法
-
超时机制
为资源请求设置超时时间 Timeout,超时后释放资源重试 if wait_time>Tout→release_resources()
- 检测与恢复
-
死锁检测
周期性地构建资源分配图(RAG),检测环路存在P1-->|Hold| R1 P2-->|Hold| R2 R1-->|Wait| P2 R2-->|Wait| P1 -
恢复机制
- 终止所有死锁进程(简单但代价高)
- 逐个终止进程直至死锁解除(按优先级/执行时间选择)
4.最佳实践(降低死锁概率)
- 统一访问顺序
所有线程按相同顺序请求资源(破坏循环等待) - 减少锁持有时间
事务尽量简短,避免在临界区执行耗时操作 - 使用尝试锁
如ReentrantLock.tryLock()替代阻塞等待 - 设置资源层级
定义资源获取的优先级顺序(如 R1→R2→R3)
📊 统计发现:80%的死锁可通过破坏循环等待条件避免,银行家算法可预防剩余15%的死锁
16.MySQL锁表排查
一、初步确认锁表现象(系统级检查)
-
查看表锁争夺状态
通过状态变量分析表锁的等待情况(高Table_locks_waited值表明锁竞争严重):SHOW STATUS LIKE 'Table%';+----------------------------+---------+ | Variable_name | Value | +----------------------------+---------+ | Table_locks_immediate | 105 | -- 立即获得表锁的次数 | Table_locks_waited | 3 | -- 等待表锁的次数(关键指标) +----------------------------+---------+阈值参考:若
Table_locks_waited持续增长或占比超过5%,需深入排查。 -
检查进程阻塞状态
查看当前所有连接的状态,定位阻塞进程(State列为Waiting for table lock):SHOW FULL PROCESSLIST;+----+------+-----------+------+---------+------+-------------------------+-----------------------+ | Id | User | Host | db | Command | Time | State | Info | +----+------+-----------+------+---------+------+-------------------------+-----------------------+ | 7 | root | localhost | test | Query | 50 | Waiting for table lock | SELECT * FROM orders | +----+------+-----------+------+---------+------+-------------------------+-----------------------+
二、精准定位锁表源(InnoDB引擎)
-
查询锁等待关系
获取正在等待锁的事务及其执行的SQL语句3:SELECT it.trx_query AS blocked_sql, -- 被阻塞的SQL ilw.requesting_trx_id, -- 请求锁的事务ID ilw.blocking_trx_id -- 持有锁的事务ID FROM information_schema.innodb_trx it JOIN information_schema.innodb_lock_waits ilw ON it.trx_id = ilw.requesting_trx_id;输出示例:
+-----------------------------+-------------------+------------------+ | blocked_sql | requesting_trx_id | blocking_trx_id | +-----------------------------+-------------------+------------------+ | UPDATE orders SET amt=100 | 123456 | 789012 | +-----------------------------+-------------------+------------------+ -
分析事务锁详情
查看所有活跃事务的锁信息:SELECT trx_id, trx_state, trx_query, trx_rows_locked, -- 已锁定的行数 trx_mysql_thread_id -- 对应线程ID FROM information_schema.innodb_trx; -
检查表级锁
明确哪些表被锁定:SELECT * FROM performance_schema.metadata_locks WHERE OWNER_THREAD_ID IN ( SELECT THREAD_ID FROM performance_schema.threads WHERE PROCESSLIST_ID = [blocking_trx_id] );
三、解决锁表问题(应急与优化)
-
强制终止阻塞事务
根据trx_mysql_thread_id终止持有锁的事务:KILL [blocking_thread_id]; -- 例如 KILL 789012 -
优化策略
- 减小锁定范围:避免全表更新(如
UPDATE WHERE id=1而非UPDATE WHERE name='abc') - 控制事务时长:长事务拆分为短事务,减少锁持有时间1
- 调整隔离级别:从
REPEATABLE READ降级为READ COMMITTED - CDC同步优化:全量同步时使用快照替代锁表 四、预防锁表(监控与设计)
- 减小锁定范围:避免全表更新(如
| 措施 | 实现方法 |
|---|---|
| 实时监控 | 部署Prometheus+Granafa监控Table_locks_waited波动 |
| 锁超时机制 | 设置innodb_lock_wait_timeout=5(默认50秒) |
| 索引优化 | 确保WHERE条件使用索引,减少行锁升级为表锁的概率 |
| 读写分离 | 高危操作(如ALTER TABLE)切换到从库执行4 |
⚠️ 特殊场景:CDC全量同步锁表时,建议在业务低峰期操作或使用
FLUSH TABLES WITH READ LOCK短暂锁定
17.线程与进程
线程和进程的核心区别在于资源隔离性:进程独立性强,但开销大;线程共享资源,效率高但风险高。在实际开发中,选择线程或进程需权衡资源管理、并发需求和容错性。例如,一个应用程序(如视频编辑器)可能启动一个主进程,内部创建多个线程处理渲染任务。
18.三个线程如何交替执行任务?
package top.arhi.test.thread.Demo.test3;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ABCTest {
private static final int MAX_NUM = 30;
private static int count = 1;
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition[] conditions = new Condition[3];
static {
for (int i = 0; i < 3; i++) {
conditions[i] = lock.newCondition();
}
}
static class PrintTask implements Runnable {
private final int threadId;
public PrintTask(int threadId) {
this.threadId = threadId;
}
@Override
public void run() {
while (count <= MAX_NUM) {
lock.lock();
try {
// 检查是否轮到自己执行
while (count % 3 != threadId && count <= MAX_NUM) {
conditions[threadId].await();
}
if (count <= MAX_NUM) {
System.out.println("Thread-" + (threadId + 1) + ": " + count++);
}
// 唤醒下一个线程
conditions[(threadId + 1) % 3].signal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
}
public static void main(String[] args) {
new Thread(new PrintTask(1), "Thread-2").start();
new Thread(new PrintTask(2), "Thread-3").start();
new Thread(new PrintTask(0), "Thread-1").start(); // 最后启动Thread-1
}
}
package top.arhi.test.thread.Demo.test2;
public class ABCTest {
static volatile Integer count = 1;
public static void main(String[] args) {
new Thread(() -> {
try {
for (int i = 0; i < 10; ) {
while (count % 3 != 1) {
}
synchronized (count) {
if (count % 3 == 1) {
System.out.println(Thread.currentThread().getName() + "-----" + "A");
count++;
i++;
}
}
}
} catch (Exception e) {
}
}, "A1").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; ) {
while (count % 3 != 2) {
}
synchronized (count) {
if (count % 3 == 2) {
System.out.println(Thread.currentThread().getName() + "-----" + "B");
count++;
i++;
}
}
}
} catch (Exception e) {
}
}, "B1").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; ) {
while (count % 3 != 0) {
}
synchronized (count) {
if (count % 3 == 0) {
System.out.println(Thread.currentThread().getName() + "-----" + "C");
count++;
i++;
}
}
}
} catch (Exception e) {
}
}, "C1").start();
}
}
19.线程编排
- 使用线程池和Future(基础):通过ExecutorService提交任务并获得Future对象,可以获取异步执行结果,但编排复杂任务时比较繁琐。
- CompletableFuture(推荐):Java 8引入,提供丰富的API来编排异步任务,支持链式调用、组合多个任务、异常处理等。
- 第三方框架:如RxJava、Reactor等响应式编程库,提供更强大的异步编排能力
Java线程编排的核心是CompletableFuture,它提供了:
- 链式调用:
thenApply,thenAccept,thenRun - 任务组合:
thenCombine,applyToEither,allOf,anyOf - 异常处理:
exceptionally,handle
这种方法比传统的Future+Callback更简洁,避免了回调地狱。
20.线程通信方式
- 共享变量(使用synchronized和volatile)
- 等待/通知机制(wait/notify)
- 管道流(PipedInputStream/PipedOutputStream)
- 高级并发工具(如Lock和Condition、BlockingQueue等)
| 方式 | 实时性 | 复杂度 | 数据容量 | 适用场景 |
|---|---|---|---|---|
| 共享变量 | 高 | 低 | 小 | 简单状态同步 |
| 等待/通知 | 中 | 中 | 中 | 生产者-消费者模型 |
| 管道流 | 低 | 高 | 大 | 流式数据传输 |
BlockingQueue | 高 | 低 | 可配置 | 高并发任务分发 |