Java面试-04-Java多线程与并发

Java多线程与并发面试题

目录

  • [1. 创建线程的方式](#1. 创建线程的方式)
  • [2. 线程生命周期(6大状态)](#2. 线程生命周期(6大状态))
  • [3. 线程池](#3. 线程池)
    • [3.1 创建线程池的方式](#3.1 创建线程池的方式)
    • [3.2 为什么不建议用Executors创建线程池](#3.2 为什么不建议用Executors创建线程池)
    • [3.3 线程池的核心参数](#3.3 线程池的核心参数)
    • [3.4 线程池的执行原理](#3.4 线程池的执行原理)
    • [3.5 任务类型特点与线程数分配](#3.5 任务类型特点与线程数分配)
    • [3.6 submit与execute的区别](#3.6 submit与execute的区别)
    • [3.7 线程池拒绝策略](#3.7 线程池拒绝策略)
  • [4. 线程常用API方法](#4. 线程常用API方法)
  • [5. 程序中怎么保证多线程的运行安全](#5. 程序中怎么保证多线程的运行安全)
    • [5.1 使用synchronized关键字](#5.1 使用synchronized关键字)
    • [5.2 使用Lock接口](#5.2 使用Lock接口)
    • [5.3 使用原子类](#5.3 使用原子类)
    • [5.4 使用volatile关键字](#5.4 使用volatile关键字)
    • [5.5 使用线程安全集合](#5.5 使用线程安全集合)
    • [5.6 使用ThreadLocal](#5.6 使用ThreadLocal)
    • [5.7 避免共享可变状态](#5.7 避免共享可变状态)
    • [5.8 使用并发工具类](#5.8 使用并发工具类)
  • [6. Java内存模型(JMM)](#6. Java内存模型(JMM))
  • [7. 并发核心关键字](#7. 并发核心关键字)
  • [8. 锁机制详解](#8. 锁机制详解)
  • [9. CAS与原子类](#9. CAS与原子类)
  • [10. ThreadLocal](#10. ThreadLocal)
  • [11. 线程安全集合](#11. 线程安全集合)
  • [12. 并发队列](#12. 并发队列)
  • [13. JUC并发工具类](#13. JUC并发工具类)
  • [14. 死锁与排查](#14. 死锁与排查)

1. 创建线程的方式

方式一:继承Thread类

定义一个类继承Thread类,重写run方法,在run方法中编写线程执行逻辑。使用时创建该类的实例并调用start方法启动线程。这种方式的缺点是Java单继承限制,无法再继承其他类。

方式二:实现Runnable接口

定义一个类实现Runnable接口,实现run方法。使用时将该类的实例作为参数传给Thread构造器,然后调用start方法。这种方式更灵活,不影响类继承其他类,是推荐的方式。

方式三:实现Callable接口(有返回值)

定义一个类实现Callable接口,实现call方法,该方法可以返回结果并抛出异常。使用时需要借助FutureTask包装Callable对象,再将FutureTask传给Thread。通过FutureTask的get方法可以获取线程执行的返回值。

方式四:线程池创建(推荐)

通过Executors工具类创建线程池,调用submit方法提交任务。线程池会自动管理线程的创建、复用和销毁,适用于高并发场景。使用完毕后调用shutdown方法关闭线程池。

四种方式对比

方式 是否可继承其他类 是否有返回值 适用场景
继承Thread 简单场景
实现Runnable 推荐,灵活
实现Callable 需要返回结果
线程池 高并发场景

2. 线程生命周期(6大状态)

  1. 新建(New):创建线程对象但尚未启动
  2. 就绪(Runnable):调用start方法后,线程进入就绪队列等待CPU调度
  3. 运行(Running):获得CPU时间片,开始执行run方法中的逻辑
  4. 阻塞(Blocked):等待锁、进行IO操作或等待被唤醒
  5. 等待(Waiting):调用wait或join方法后进入无限等待状态
  6. 超时等待(TimedWaiting):调用sleep等方法后进入限时等待状态
  7. 终止(Terminated):线程执行完毕或因异常退出

阻塞分类

  • 同步阻塞:线程争夺synchronized锁失败时进入的阻塞状态
  • 等待阻塞:调用wait方法后进入等待状态,需要notify方法唤醒
  • 其他阻塞:调用sleep、join方法或进行IO操作时进入的阻塞状态

3. 线程池

3.1 创建线程池的方式

方式一:使用Executors工厂方法(不推荐)

通过Executors工具类提供的静态方法创建线程池,主要包括:固定大小线程池、单线程线程池、缓存线程池和定时任务线程池。这种方式虽然方便,但存在资源耗尽的风险。

方式二:使用ThreadPoolExecutor(推荐)

手动创建ThreadPoolExecutor实例,传入七个核心参数:核心线程数、最大线程数、空闲线程存活时间、时间单位、任务队列、线程工厂和拒绝策略。这种方式可以精确控制线程池的行为,避免资源耗尽风险。

3.2 为什么不建议用Executors创建线程池

方法 问题 风险
newFixedThreadPool/newSingleThreadExecutor 使用无界队列LinkedBlockingQueue 任务过多时队列无限增长,可能OOM
newCachedThreadPool 线程数无上限 线程过多导致CPU耗尽或OOM
newScheduledThreadPool 同样使用无界队列 任务过多时队列无限增长

结论 :生产环境推荐使用ThreadPoolExecutor手动配置参数,避免资源耗尽风险。

3.3 线程池的核心参数

参数 含义 作用
corePoolSize 核心线程数 线程池维护的最小线程数,即使空闲也不会回收
maximumPoolSize 最大线程数 线程池允许的最大线程数
keepAliveTime 空闲线程存活时间 非核心线程空闲超过此时间会被回收
unit 时间单位 keepAliveTime的单位(秒、毫秒等)
workQueue 任务队列 存放等待执行的任务
threadFactory 线程工厂 创建新线程的工厂
handler 拒绝策略 任务无法处理时的处理策略

3.4 线程池的执行原理

线程池的执行遵循以下流程:当提交任务时,首先检查核心线程池是否已满,如果未满则创建核心线程执行任务;如果核心线程已满,则检查任务队列是否已满,如果未满则将任务加入队列等待;如果队列也已满,则检查线程池是否达到最大线程数,如果未达到则创建非核心线程执行任务;如果线程池已满,则执行拒绝策略。

执行流程详解

  1. 当提交任务时,优先创建核心线程执行
  2. 核心线程满后,任务进入队列等待
  3. 队列满后,创建非核心线程执行
  4. 所有线程都在忙且队列满时,触发拒绝策略

3.5 任务类型特点与线程数分配

任务类型 特点 计算公式 参数说明
CPU密集型 大量计算、CPU占用高、IO等待少 线程数 = CPU核心数 + 1 核心数:Runtime.getRuntime().availableProcessors()
IO密集型 大量IO等待、CPU空闲 线程数 = CPU核心数 × (1 + 平均等待时间/平均工作时间) 等待时间:线程阻塞IO时间;工作时间:CPU计算时间
混合型 计算+IO都有 无固定公式,取中间值微调 参考IO密集型参数

3.6 submit与execute的区别

特性 execute submit
参数类型 Runnable Runnable / Callable<T>
返回值 void Future<T>
异常处理 直接抛出 异常被封装到Future中
使用场景 无需返回结果 需要获取执行结果

使用示例:execute方法用于提交不需要返回结果的任务,直接执行即可;submit方法用于提交需要返回结果的任务,返回一个Future对象,通过该对象可以获取任务执行的返回值。

3.7 线程池拒绝策略

拒绝策略 行为 适用场景
AbortPolicy(终止) 直接抛出异常 任务不能丢失,必须执行
DiscardPolicy(丢弃) 静默丢弃任务 任务可丢失,不影响业务
CallerRunsPolicy(调用者执行) 任务返回给提交者执行 调用者可承担执行压力
DiscardOldestPolicy(丢弃最老) 丢弃队列最旧任务,加入新任务 新任务比旧任务更重要
自定义拒绝策略 实现RejectedExecutionHandler 存入DB/缓存,延后执行

4. 线程常用API方法

方法 作用
currentThread() 获取当前线程对象
start() 启动线程,进入就绪态
sleep(long) 休眠,让出CPU,不释放锁
join() 等待该线程执行完毕
wait() 等待,释放锁(同步块中)
notify() 随机唤醒一个等待线程
notifyAll() 唤醒所有等待线程
setDaemon(true) 设置守护线程(必须在start前)
isAlive() 判断线程是否存活
setPriority(int) 设置优先级(1-10,默认5)

5. 程序中怎么保证多线程的运行安全

5.1 使用synchronized关键字

修饰实例方法:将synchronized关键字加到实例方法上,此时锁的是当前对象实例,同一时刻只有一个线程可以执行该方法。

修饰静态方法:将synchronized关键字加到静态方法上,此时锁的是类对象,同一时刻只有一个线程可以执行该类的任意静态同步方法。

修饰代码块:使用synchronized关键字修饰代码块,明确指定锁对象,可以是this、类对象或其他任意对象,只对代码块内的代码进行同步。

5.2 使用Lock接口

创建Lock接口的实现类实例,如ReentrantLock。在需要同步的代码前调用lock方法获取锁,在finally块中调用unlock方法释放锁,确保锁一定会被释放。Lock接口提供了比synchronized更灵活的锁机制,支持公平锁、可中断锁和超时获取锁等特性。

5.3 使用原子类

使用java.util.concurrent.atomic包下的原子类,如AtomicInteger、AtomicLong等。这些类提供了原子操作方法,可以在不使用锁的情况下保证操作的原子性,通过CAS机制实现线程安全。

5.4 使用volatile关键字

将共享变量声明为volatile类型,可以保证变量的可见性和禁止指令重排序,但不能保证操作的原子性。适用于状态标记量等场景。

5.5 使用线程安全集合

使用java.util.concurrent包下的线程安全集合,如CopyOnWriteArrayList、ConcurrentHashMap等。这些集合内部实现了线程安全机制,可以在多线程环境下安全地进行操作。

5.6 使用ThreadLocal

创建ThreadLocal实例来存储线程私有数据,每个线程都有自己独立的副本,互不干扰。适用于存储用户上下文、数据库连接等需要线程隔离的数据。使用完毕后应调用remove方法避免内存泄漏。

5.7 避免共享可变状态

通过将可变状态封装在方法内部作为局部变量,或者使用不可变对象,或者采用消息传递的方式代替共享内存,可以从根本上避免线程安全问题。

5.8 使用并发工具类

使用java.util.concurrent包下的并发工具类,如CountDownLatch、CyclicBarrier、Semaphore等,可以方便地实现线程间的同步和协作,避免手动编写复杂的同步逻辑。

5.9 线程安全保障方法对比

方法 保障特性 适用场景
synchronized 原子性、可见性、有序性 通用场景
Lock 原子性、可见性、有序性 需要高级特性(中断、超时)
volatile 可见性、有序性 状态标记量
原子类 原子性、可见性 计数器、状态标志
线程安全集合 集合操作线程安全 共享集合操作
ThreadLocal 线程隔离 线程私有数据

6. Java内存模型(JMM)

什么是JMM

Java内存模型,统一多线程内存访问规则,保证可见性、原子性、有序性

主内存 vs 工作内存

  • 主内存:所有线程共享,存储共享变量
  • 工作内存:线程私有,操作变量副本
  • 线程读写流程:主内存 → 工作内存 → 主内存

JMM三大特性

  1. 可见性 :一个线程修改,其他线程立即感知
    • 保证:volatilesynchronizedLock
  2. 原子性 :操作不可中断
    • 保证:synchronized、原子类
  3. 有序性 :禁止指令重排
    • 保证:volatilesynchronized

指令重排

  • 编译器/CPU优化执行顺序,单线程安全,多线程不安全
  • 解决方案:volatile禁止重排

7. 并发核心关键字

volatile

  • 两大作用:保证可见性禁止指令重排
  • 不能保证原子性(不能解决i++并发问题)
  • 适用:状态标记量、DCL单例

synchronized

  • 作用:保证原子性、可见性、有序性
  • 使用:修饰实例方法、静态方法、代码块
  • 原理:
    • 加锁:自动获取锁
    • 解锁:自动释放锁
    • 释放锁时刷新到主内存,获取锁时重读主内存

Lock 与 synchronized 区别

synchronized Lock
Java关键字 接口
自动加锁/解锁 手动lock()/unlock()(finally释放)
非公平锁 支持公平/非公平
无法中断 支持中断
无超时机制 支持超时获取锁

8. 锁机制详解

悲观锁 vs 乐观锁

  • 悲观锁 :先加锁再操作(synchronized、Lock)
    • 适用:写多读少
  • 乐观锁 :不加锁,更新时版本校验(CAS、版本号)
    • 适用:读多写少

公平锁 vs 非公平锁

  • 公平锁:按申请顺序获取锁,无饥饿,效率低
  • 非公平锁:可插队,效率高,可能产生饥饿

可重入锁

  • 同一个线程可再次获取自己持有的锁
  • 示例:synchronizedReentrantLock

自旋锁

  • 获取锁失败时循环重试,不阻塞线程
  • 优点:短时间加锁效率高
  • 缺点:长时间占用CPU

读写锁(ReadWriteLock)

  • 读锁:共享,多线程可同时读
  • 写锁:独占,写时阻塞所有操作
  • 适用:读多写少

AQS(AbstractQueuedSynchronizer)

  1. 定位
    AQS 是 JUC 并发包的核心基础框架,是锁、同步器的统一底层实现,ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier、ReentrantReadWriteLock 全部基于 AQS 实现。
  2. 三大核心组成
  • state 同步状态(volatile int)
    用一个volatile修饰的 int 变量,保证多线程可见性
    不同组件含义不同:
    ReentrantLock:state=0无锁,state≥1持有锁(重入次数)
    Semaphore:剩余可用许可数
    CountDownLatch:剩余需要倒数的次数
  • 双向链表结构的 CLH 变种阻塞队列
    存储获取锁失败、需要等待的线程
    节点:Node,包含线程对象、等待状态、前驱 / 后继指针
    头节点:持有锁的线程(或空节点),唤醒后继节点
    尾节点:新入队线程追加到尾部
  • 独占 / 共享两种模式
    独占模式(EXCLUSIVE):同一时间只有一个线程持有锁 → ReentrantLock
    共享模式(SHARED):多个线程可同时持有 → Semaphore/CountDownLatch
  1. 核心原理(一句话)
    线程尝试获取锁失败 → 封装成 Node 加入阻塞队列 → 阻塞等待 → 锁释放时唤醒队列头节点线程 → 重新竞争锁
  2. 关键底层操作
    CAS 无锁算法:修改 state、队列头尾节点,不加锁保证原子性
    LockSupport.park()/unpark():线程阻塞 / 唤醒(比 wait/notify 更高效)
    volatile:保证 state 和队列节点的内存可见性,防止指令重排

ReentrantLock 原理

1. 核心定位

ReentrantLock 是基于 AQS 实现的可重入独占锁,支持公平和非公平两种模式

2. 公平锁 vs 非公平锁
特性 公平锁 非公平锁
锁获取顺序 按队列顺序 可插队
性能 较低(需维护队列顺序) 较高
饥饿问题 可能产生
默认值 非公平锁 -

创建方式:

创建 ReentrantLock 实例时,默认是非公平锁;传入 true 参数可以创建公平锁,传入 false 参数或不传参数创建非公平锁。

3. 锁获取流程

非公平锁获取:

线程尝试获取锁时,首先通过 CAS 操作尝试将 state 从 0 设置为 1。如果成功则获取锁;如果失败,则判断当前线程是否为锁持有者,如果是则重入成功,state 加 1;如果不是则加入阻塞队列等待。

公平锁获取:

线程尝试获取锁时,首先检查队列中是否有等待的线程。如果有则直接加入队列等待,不插队;如果没有则通过 CAS 操作尝试获取锁,成功则获取锁,失败则加入队列等待。

4. 锁释放流程

线程调用 unlock 方法时,state 减 1。如果 state 减到 0,则释放锁并唤醒队列头节点的线程;如果 state 大于 0,则仅减少重入次数,锁仍被当前线程持有。


9. CAS与原子类

CAS(Compare-And-Swap)

  • 无锁原子操作:V(内存值)、A(预期值)、B(新值)
  • V == A → 更新为B;否则不操作
    是一种无锁机制,(Compare-and-Swap)比较并交换,CAS操作包含三个操作数 ------ 内存位置(V)、期望的原值(A)和新值(B)。执行CAS操作时,会将内存位置V的值与期望的原值A进行比较。如果相匹配,那么处理器会自动将该内存位置V的值更新为新值B。如果不匹配,处理器不做任何操作。

CAS缺点

  1. ABA问题
    CAS操作虽然可以确保原子性,但存在所谓的"ABA问题"。假设一个线程读取了一个变量的值A,然后做了一些计算或等待。在此期间,另一个线程将变量的值从A改为B,然后又改回A。当第一个线程回来准备使用CAS更新值时,它会发现变量的值仍然是A,所以CAS操作会成功。然而,这实际上是一个问题,因为在这段时间内,变量的值已经被另一个线程更改过。解决这个问题的常用方法是使用版本号或时间戳,即变量不仅保存实际值,还保存一个表示何时更改过的标识符。
  2. 长时间自旋消耗CPU
  3. 只能保证单个变量原子性

原子类(java.util.concurrent.atomic)

类名 作用
AtomicInteger 整数原子操作
AtomicLong 长整型原子操作
AtomicBoolean 布尔原子操作
AtomicReference 引用类型原子操作

10. ThreadLocal

  • 介绍:ThreadLocal提供线程局部变量,为各线程创建独立副本,线程操作互不干扰,像给线程配私有 "小箱子"。适用场景:数据库连接管理,为多线程数据库操作提供独立连接;
  • 应用:Web 应用里保存用户身份信息,避免方法间传参。
  • 问题:内存泄漏问题:ThreadLocalMap键为ThreadLocal弱引用,值是强引用。外部强引用消失,ThreadLocal被回收,键变null,值无法回收。解决办法是使用完调用remove()移除键值对。

11. 线程安全集合

集合 实现 特点
Vector synchronized 低效,基本废弃
Hashtable synchronized 低效,基本废弃
ConcurrentHashMap CAS + synchronized 高并发、推荐
CopyOnWriteArrayList 写时复制 读多写少
CopyOnWriteArraySet 写时复制 读多写少
ConcurrentSkipListMap 跳表 线程安全、有序

12. 并发队列

队列 类型 特点
ConcurrentLinkedQueue 无界非阻塞 高并发、无锁
ArrayBlockingQueue 有界阻塞 数组实现
LinkedBlockingQueue 无界/有界阻塞 链表实现
PriorityBlockingQueue 优先级阻塞 按优先级出队
DelayQueue 延时阻塞 到期才能取出

13. JUC并发工具类

CountDownLatch

  • 计数器,减到0唤醒等待线程
  • 不可重复使用
  • 场景:主线程等待多线程完成

CyclicBarrier

  • 线程相互等待,集齐数量一起执行
  • 可循环使用
  • 场景:分组并发执行

Semaphore

  • 控制最大并发线程数
  • 场景:接口限流、连接池控制

14. 死锁与排查

死锁四个必要条件

  1. 互斥:资源独占
  2. 请求与保持:持有锁并等待其他锁
  3. 不可剥夺:锁不能被强抢
  4. 循环等待:线程形成环形等待链

避免死锁

  1. 统一加锁顺序
  2. 设置锁超时
  3. 减少锁嵌套

死锁排查步骤

  1. 使用top或ps命令找到Java进程的PID
  2. 使用jstack命令加上PID参数导出线程栈信息到日志文件
  3. 在导出的线程栈文件中搜索Found one Java-level deadlock关键字
  4. 根据线程栈信息定位死锁代码,调整加锁顺序解决死锁问题