Java多线程面试题

104 阅读19分钟

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()阻塞等待的时候,此时等待的那个线程运行结束或者waitnotify唤醒时,就会从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()方法的区别

  1. 来源不同:

    • wait()Object 类的方法。
    • sleep()Thread 类的静态方法。
  2. 锁的行为:

    • wait() 方法调用后,当前线程会释放它所持有的对象锁(monitor)。
    • sleep() 方法调用后,当前线程不会释放任何锁,仍然持有锁。
  3. 使用条件:

    • wait() 必须在同步块(synchronized方法或同步代码块)中使用,否则会抛出IllegalMonitorStateException
    • sleep() 可以在任何地方使用,不需要在同步块中。
  4. 唤醒机制:

    • wait() 可以被其他线程通过调用同一个对象的notify()notifyAll()方法来唤醒。
    • sleep() 在指定的时间过后会自动唤醒,或者可以通过调用该线程的interrupt()方法来中断休眠,此时会抛出InterruptedException
  5. 线程状态:

    • wait() 会让线程进入等待状态(WAITING或TIMED_WAITING,如果指定了超时时间),直到被唤醒。
    • sleep() 会让线程进入睡眠状态(TIMED_WAITING),直到时间到期或被中断。
  6. 用途:

    • wait() 通常用于线程间通信,等待某个条件满足。
    • sleep() 通常用于暂停执行一段时间。

6.notify()与notifyAll()区别

  1. notify()和notifyAll()都是Object类的方法,用于唤醒在对象上等待的线程。

  2. 区别:

    • notify():随机唤醒一个在该对象上等待的线程(具体哪个线程取决于JVM实现),被唤醒的线程将从等待队列(WAITING)移到同步队列(BLOCKED),并尝试获取对象锁。
    • notifyAll():唤醒所有在该对象上等待的线程,所有被唤醒的线程都将从等待队列移到同步队列,然后竞争对象锁。
  3. 注意事项:

    • 使用notify()时,如果多个线程在等待,而只唤醒其中一个,那么其他线程可能会一直等待下去(直到再次被通知),这可能导致“线程饥饿”。
    • 使用notifyAll()会唤醒所有等待线程,但只有一个线程能获得锁,其余线程会继续阻塞在同步队列中,这可能会造成一定的性能开销(因为很多线程被唤醒但只能有一个执行,其余又会被阻塞)。
  4. 使用场景:

    • 当所有等待线程都是同质的(即它们等待的条件相同,任何一个被唤醒都能执行)时,使用notify()效率更高。
    • 当等待线程是异质的(它们等待的条件可能不同,需要唤醒所有线程来检查各自的条件)时,必须使用notifyAll(),以避免有线程永远不被唤醒。
  5. 最佳实践:

    • 通常,我们建议使用notifyAll(),因为它更安全,可以避免有线程被遗漏唤醒。但是,在明确知道只有一个线程需要被唤醒,或者所有等待线程都是同质的情况下,可以使用notify()以提升性能。

7.线程出现安全问题的判断依据

判断依据:
当同时满足以下三个条件时,即可判断存在线程安全问题:

  1. 多线程环境
  2. 共享资源访问
  3. 至少一个线程执行写操作
  4. 无适当同步机制保障(原子性、可见性、有序性)

💡 最佳实践

  • 使用synchronizedjava.util.concurrent.locks包下的锁机制保证原子性。
  • 使用volatile关键字或锁保证可见性。
  • 避免指令重排序(如使用final修饰不可变对象,或利用volatile禁止重排序)

8.虚拟线程有哪些好处

  1. 资源高效:虚拟线程消耗的内存和CPU资源远少于平台线程(传统线程),使得创建大量线程成为可能(甚至数百万个)。
  2. 高并发:通过将大量虚拟线程映射到少量操作系统线程,可以支持极高的并发量,特别适合处理大量IO操作(如网络请求、文件读写)。
  3. 简化编程模型:开发者可以像编写顺序代码一样编写并发代码,而不需要复杂的线程池管理和回调地狱。
  4. 减少上下文切换开销:虚拟线程的挂起和恢复由JVM管理,发生在用户空间,不涉及操作系统内核,因此切换代价极小。
  5. 避免常见并发问题:由于虚拟线程的设计,可以减少死锁、内存泄漏等问题,并且通过结构化并发(Structured Concurrency)可以更安全地管理线程生命周期

9.线程池创建的几个参数

  1. corePoolSize: 核心线程数,即线程池中保持存活的最小线程数量,即使这些线程处于空闲状态。
  2. maximumPoolSize: 最大线程数,线程池允许创建的最大线程数量。
  3. keepAliveTime: 非核心线程的空闲存活时间。当线程池中的线程数量超过corePoolSize时,多余的空闲线程在等待新任务的最长时间,超过这个时间它们将被终止。
  4. unit: keepAliveTime参数的时间单位(如TimeUnit.SECONDS)。
  5. workQueue: 任务队列,用于保存等待执行的任务的阻塞队列。常用的有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等。
  6. threadFactory: 线程工厂,用于创建新线程。可以自定义线程的名称、优先级等。
  7. 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的参数配置参考

  1. 核心线程数设置依据:

    • CPU核心数:通过Runtime.getRuntime().availableProcessors()获取,记为NN
    • 任务类型:
      • CPU密集型任务:线程数≈N+1
      • IO密集型任务:线程数≈2N
    • 混合型任务: 线程数 ≈ CPU线程数 × (1 + I/O耗时 / CPU耗时)
  2. 等待时间(keepAliveTime)设置:

    • 非核心线程空闲超过此时间将被回收
    • 设置原则:
      • 频繁波动场景:设置较短时间(如30-60秒)避免资源浪费
      • 稳定负载场景:可适当延长(如几分钟)减少线程重建开销
      • 特殊需求:若需维持线程池最小处理能力,可将corePoolSize设为0,全部线程使用keepAliveTime回收(需配合allowCoreThreadTimeOut参数)
  3. 注意事项:

    • 最大线程数(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.锁实现机制对比

特性synchronizedReentrantLock
实现原理JVM 内置监视器锁(Monitor)基于 AQS(AbstractQueuedSynchronizer)的 API 实现
锁类型非公平锁(默认)支持公平锁和非公平锁(通过构造函数指定)
锁获取方式自动获取和释放需显式调用 lock() 和 unlock()
底层依赖JVM 原生支持JDK 层面的 API 实现

2.异常处理对比

场景synchronizedReentrantLock
同步块内异常自动释放锁需在 finally 中手动释放锁
死锁风险较高(不可中断)较低(支持尝试获取锁)

3.适用场景对比

场景推荐方案原因
简单同步逻辑synchronized简洁安全,自动管理锁
需要公平锁ReentrantLock构造函数指定公平策略
跨方法锁传递(锁链)ReentrantLock可灵活控制锁边界
分组唤醒等待线程ReentrantLock+Condition多个条件队列支持
高并发竞争优化ReentrantLockCAS 减少线程切换

13.悲观锁与乐观锁

悲观锁

对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

乐观锁

乐观锁,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。一旦多个线程发生冲突,乐观锁通常使用一种称为 CAS 的技术来保证线程执行的安全性。

14.什么是CAS

在 CAS 中,有这样三个值:

  • V:要更新的变量(var)
  • E:预期值(expected)
  • N:新值(new)

比较并交换的过程如下:

判断 V 是否等于 E,如果等于,将 V 的值设置为 N;如果不等,说明已经有其它线程更新了 V,于是当前线程放弃更新,什么都不做。

这里的预期值 E 本质上指的是“旧值”

15.什么是死锁,该如何避免

死锁是多进程/多线程系统中的一种僵持状态,当两个或多个进程在执行过程中,因争夺资源而陷入相互等待的阻塞状态。每个进程都持有部分资源,同时等待其他进程释放资源,若无外力干预,所有进程将无限期等待无法推进

  1. 预防策略(破坏必要条件)
目标方法实现示例
破坏占有并等待一次性申请所有资源事务开始时申请全部所需资源
破坏不可剥夺允许强制回收资源优先级高的进程可抢占资源
破坏循环等待资源有序分配法所有进程按固定顺序请求资源
  1. 动态避免策略
  • 银行家算法

  • 超时机制
    为资源请求设置超时时间 Timeout​,超时后释放资源重试 if wait_time>Tout→release_resources()

  1. 检测与恢复
  • 死锁检测
    周期性地构建资源分配图(RAG),检测环路存在

      P1-->|Hold| R1
      P2-->|Hold| R2
      R1-->|Wait| P2
      R2-->|Wait| P1
    
  • 恢复机制

    • 终止所有死锁进程(简单但代价高)
    • 逐个终止进程直至死锁解除(按优先级/执行时间选择)

4.最佳实践(降低死锁概率)

  1. 统一访问顺序
    所有线程按相同顺序请求资源(破坏循环等待)
  2. 减少锁持有时间
    事务尽量简短,避免在临界区执行耗时操作
  3. 使用尝试锁
    ReentrantLock.tryLock() 替代阻塞等待
  4. 设置资源层级
    定义资源获取的优先级顺序(如 R1→R2→R3)

📊 统计发现:80%的死锁可通过破坏循环等待条件避免,银行家算法可预防剩余15%的死锁

16.MySQL锁表排查

一、初步确认锁表现象(系统级检查)

  1. 查看表锁争夺状态
    通过状态变量分析表锁的等待情况(高Table_locks_waited值表明锁竞争严重):

    SHOW STATUS LIKE 'Table%';
    
    +----------------------------+---------+
    | Variable_name              | Value   |
    +----------------------------+---------+
    | Table_locks_immediate      | 105     |  -- 立即获得表锁的次数
    | Table_locks_waited         | 3       |  -- 等待表锁的次数(关键指标)
    +----------------------------+---------+
    

    阈值参考:若Table_locks_waited持续增长或占比超过5%,需深入排查。

  2. 检查进程阻塞状态
    查看当前所有连接的状态,定位阻塞进程(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引擎)

  1. 查询锁等待关系
    获取正在等待锁的事务及其执行的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           | 
    +-----------------------------+-------------------+------------------+
    
  2. 分析事务锁详情
    查看所有活跃事务的锁信息:

    SELECT 
      trx_id, 
      trx_state, 
      trx_query, 
      trx_rows_locked,  -- 已锁定的行数
      trx_mysql_thread_id -- 对应线程ID
    FROM information_schema.innodb_trx;
    
  3. 检查表级锁
    明确哪些表被锁定:

    SELECT * 
    FROM performance_schema.metadata_locks 
    WHERE OWNER_THREAD_ID IN (
      SELECT THREAD_ID 
      FROM performance_schema.threads 
      WHERE PROCESSLIST_ID = [blocking_trx_id]
    );
    

三、解决锁表问题(应急与优化)

  1. 强制终止阻塞事务
    根据trx_mysql_thread_id终止持有锁的事务:

    KILL [blocking_thread_id];  -- 例如 KILL 789012
    
  2. 优化策略

    • 减小锁定范围:避免全表更新(如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.线程编排

  1. 使用线程池和Future(基础):通过ExecutorService提交任务并获得Future对象,可以获取异步执行结果,但编排复杂任务时比较繁琐。
  2. CompletableFuture(推荐):Java 8引入,提供丰富的API来编排异步任务,支持链式调用、组合多个任务、异常处理等。
  3. 第三方框架:如RxJava、Reactor等响应式编程库,提供更强大的异步编排能力

Java线程编排的核心是CompletableFuture,它提供了:

  • 链式调用:thenApply, thenAccept, thenRun
  • 任务组合:thenCombine, applyToEither, allOf, anyOf
  • 异常处理:exceptionally, handle

这种方法比传统的Future+Callback更简洁,避免了回调地狱。

20.线程通信方式

  1. 共享变量(使用synchronized和volatile)
  2. 等待/通知机制(wait/notify)
  3. 管道流(PipedInputStream/PipedOutputStream)
  4. 高级并发工具(如Lock和Condition、BlockingQueue等)
方式实时性复杂度数据容量适用场景
共享变量简单状态同步
等待/通知生产者-消费者模型
管道流流式数据传输
BlockingQueue可配置高并发任务分发