JUC&大厂高频&后台开发-面试常考八股题

2025博客之星年度评选已开启 10w+人浏览 1.1k人参与

一、线程基础

1)⭐创建线程的三种方式?

  1. 直接使用Threa类
    a. 创建Thread子类的实例
    b. 重写run()方法,定义任务
    c. 调用start()方法启动线程
    Thread t = new Thread() {
    public void run() {
    // 要执行的任务
    }
    };

// 启动线程
t.start();

  1. 定义Runnable接口的实现类,Thread执行
    a. Runnable接口:
    ⅰ. 创建一个类,实现java.lang.Runnable接口,重写run()方法
    b. Thread类:
    ⅰ. 创建Runnable接口实现类的实例
    ⅱ. 以Runnable实例为参数构造Thread类的实例
    ⅲ. 调用Thread类的start()方法启动线程
    线程任务逻辑和类本身解耦,更利于代码的复用和维护,更符合面向对象的设计原则
    public class Task implements Runnable {
    @Override
    public void run() {
    System.out.println(“线程 " + Thread.currentThread().getName() + " 正在运行”);
    }

    public static void main(String[] args) {
    Task task = new Task();
    Thread thread = new Thread(task);
    thread.start();
    }
    }

  2. 定义Callable接口的实现类,配合FutureTask封装,Thread执行
    a. Callable接口:
    ⅰ. 创建一个类实现Callable接口,重写call()方法
    ⅱ. call()方法可以有返回值,并且可以抛出异常
    b. FutureTask类:封装
    ⅰ. 封装计算任务: FutureTask可以包装一个Callable或Runnable对象,代表一个异步执行的任务
    ⅱ. 获取计算结果: FutureTask提供了get()方法来获取异步计算的结果。这个方法会阻塞当前线程,直到计算完成并返回结果
    ⅲ. 查询任务状态: FutureTask内部维护了任务的执行状态,并允许查询这些状态(isDone()、isCancelled())
    ⅳ. 取消任务: FutureTask提供了cancel()方法来尝试取消任务的执行
    c. Thread类:
    ⅰ. 构造Thread实例时传入FutureTask实例,调用start()方法执行
    public static void main(String[] args) throws InterruptedException, ExecutionException {
    // 1. 创建一个 Callable 任务(重写接口的call方法)
    Callable task = () -> {
    // 具体任务逻辑
    };

     // 2. 创建 FutureTask 来封装 Callable 任务
     FutureTask<String> futureTask = new FutureTask<>(task);
    
     // 3. 创建一个 Thread,并将 FutureTask 传递给它
     Thread thread = new Thread(futureTask);
    
     // 4. 启动线程,开始执行任务
     thread.start();
    
     // 5. 在主线程中执行其他操作
    
     // 6. 获取异步计算的结果(这会阻塞当前线程,直到结果可用)
     String result = futureTask.get();
     System.out.println("获取到异步计算结果: " + result);
    

    }

  3. 使用线程池:
    从Java 5开始引入的java.util.concurrent.ExecutorService和相关类提供了线程池的支持,这是一种更高效的线程管理方式,避免了频繁创建和销毁线程的开销

● 缺点:
○ 增加了程序的复杂度,增加故障排查时间
○ 错误的配置可能导致死锁、资源耗尽等问题
● 优点:
○ 线程池可以重用预先创建的线程,避免了线程创建和销毁的开销,可以提高CPU利用率
○ 线程池提供线程并发来处理任务,提高系统的响应速度和吞吐量
class Task implements Runnable {
@Override
public void run() {
// 线程执行的代码
}
}

public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10); // 创建固定大小的线程池
for (int i = 0; i < 100; i++) {
executor.submit(new Task()); // 提交任务到线程池执行
}
executor.shutdown(); // 关闭线程池
}

2)⭐线程的六种状态?
从操作系统看:五种状态
在这里插入图片描述

从Java看:六种状态
在 java.lang.Thread.State枚举中定义了这六种线程状态,它们反映了一个线程在其生命周期中的不同阶段

  1. new:线程刚被创建,未调用start()方法
  2. runable:调用start()方法后进入
    a. 它表示该线程已经准备好运行,正在等待 JVM 的线程调度器为其分配 CPU 时间片
  3. blocked:阻塞
    a. 当一个线程试图获取一个被其他线程持有的锁(例如,进入一个 synchronized 修饰的方法或代码块)
  4. wait:等待
    a. 当一个线程调用了以下方法(没有指定超时时间)时,它会进入 WAITING 状态:
    ⅰ. Object.wait() (需要等待Object.notify()或Object.notifyAll()来唤醒)
    ⅱ. Thread.join() (需要等待被 join 的线程执行完毕)
    ⅲ. LockSupport.park()(需要等待LockSupport.unpark()来唤醒)
  5. timed_waiting:定时等待
    a. TIMED_WAITING状态与WAITING 状态类似,但是线程会等待指定的时间后自动醒来,或者在等待期间被其他线程唤醒
    b. 当一个线程调用了以下带有超时时间的方法之一时,它会进入 TIMED_WAITING 状态:
    ⅰ. Thread.sleep(long millis)
    ⅱ. Object.wait(long timeout)
    ⅲ. Thread.join(long millis)
    ⅳ. LockSupport.parkNanos(long nanos)
    ⅴ. LockSupport.parkUntil(long deadline)
  6. terminated:线程运行结束
    a. 当线程执行完成它的run()方法,或者由于未捕获的异常而终止时,它会进入TERMINATED状态

3)⭐使用多线程需要注意哪些问题?并发场景需要注意什么?
多线程下,需要注意线程竞争导致的数据安全问题

Java的线程安全需要考虑满足:
● 原子性:
○ 提供互斥访问,可以使用synchronized关键字等乐观锁、悲观锁机制来确保原子性
○ 可以通过事务,保证一组操作全部提交或全部回滚
● 可见性:一个线程的修改可以及时地被其他线程看到
○ 可以使用volatile关键字确保可见性

4)⭐中断线程的方式?

  1. interrupt()方法:
    a. 原理:线程内有一个boolean变量表示打断状态,初始为false,中断时为true
    b. 线程调用interrupt()方法,设置打断标记,配合代码逻辑进行检查并显示中断(如线程执行任务前,while (!Thread.currentThread().isInterrupted()) {进行判断)
    ⅰ. (显式)主动抛出异常打断线程
    ⅱ. (显式)执行return,终止线程
    ⅲ. 如果该线程正在执行低级别的可中断方法(如Thread.sleep()、Thread.join()或Object.wait()),会自动抛出中断异常打断线程
  2. 通过Future取消任务
    a. 使用线程池提交任务,并通过Future.cancel()停止线程,依赖中断机制。
  3. stop()方法(已弃用):即暴力打断线程

5)sleep()和wait()的区别?

  1. 调用模式:
    a. sleep()是Thread类的静态方法,可以在任何地方直接通过Thread.sleep()调用
    b. wait()是Object 类的实例方法,必须通过对象实例来调用
  2. 锁:
    a. sleep()不涉及锁
    b. wait()只能在同步代码块中被调用,调用时线程会释放持有的对象锁,直到其他线程调用notify()唤醒
  3. 唤醒机制:
    a. sleep()基于传递的时间,自动苏醒并开始等待cpu调度
    b. wait()需要其他线程的外部干预

6)notify()会选择哪个线程?
notify()的选择唤醒的线程是任意的,取决于具体的jvm
hot spot对notofy()的实现是“先进先出”的顺序唤醒

7)⭐什么是上下文切换?
是什么:
上下文切换 (Context Switching) 是指 CPU 从一个进程(或线程)切换到另一个进程(或线程)执行的过程

大概步骤:

  1. 保存当前进程/线程 A 的上下文
  2. 加载新进程/线程 B 的上下文
  3. 跳转到新进程/线程 B 的执行点

问题:
频繁的上下文切换,会导致系统将大量时间浪费在切换本身而非实际工作上,从而降低系统的整体吞吐量

8)⭐AQS 是什么?它的作用是什么?定义:
● 全称:AbstractQueuedSynchronizer,是 JUC 包里用来构建锁和同步器的基础框架
● 核心作用:提供了同步状态管理 + 队列等待机制

核心思想:
● int state变量表示同步状态
○ state = 0 → 无锁
○ state = 1 → 有锁
○ state可以支持计数(读写锁、信号量)
● 基于FIFO的双向队列
○ 获取锁失败的线程会被放到队列里排队等待,避免自旋浪费 CPU

两种模式:

  1. 独占模式
    ○ 同一时刻只允许一个线程持有锁
    ○ 典型应用:ReentrantLock
  2. 共享模式
    ○ 同一时刻允许多个线程获取共享资源
    ○ 典型应用:Semaphore、CountDownLatch、读写锁的读锁

使用场景:基于AQS,JDK实现了很多并发工具
● 锁类:ReentrantLock、ReentrantReadWriteLock
● 同步器:Semaphore、CountDownLatch、CyclicBarrier
● 其他:FutureTask

9)⭐volatile关键字有什么用?

  1. 保证可见性
    ● 当一个线程修改了volatile变量的值,其他线程可以立即“看到”最新值
    ● JVM会通过内存屏障保证写操作刷新到主内存,读操作从主内存拉取
  2. 禁止指令重排序
    ● 内存屏障内,会防止指令被(编译器或CPU)重排
    ● 常用于double-check-locking问题中,避免对象初始化指令重排导致synchronized错误获取
  3. 不保证原子性
    ● volatile只能保证读写单次操作的原子性
    ● 对于复合操作共同的原子性,还是要用 synchronized 或 AtomicInteger

二、并发安全

1)锁
1.1)⭐⭐Java中有哪些常见的锁?

  1. 乐观锁:并发冲突较少时,可以用乐观锁
    a. CAS操作:一种CPU无锁原子操作,在更新时检查数据是否被修改
    b. 版本号优化:基于CAS操作实现,解决ABA问题
    c. 当并发冲突频繁发生,乐观锁会导致大量的更新失败和自旋重试,应该加悲观锁
    CAS 的一个经典问题是 ABA 问题。如果一个值从 A 变成 B,然后再变回 A,对于 CAS 来说,它会认为这个值没有发生变化,但实际上已经被修改过了。版本号机制可以通过每次修改都递增版本号来避免 ABA 问题。

  2. synchronized(内置锁):Java 语言层面提供的同步机制,使用方便
    a. 引入了无锁、偏向锁、轻量级锁、重量级锁的概念,用于在不同竞争程度下,减少锁的开销【详见3)】
    b. 功能相对简单,缺乏一些高级特性(如可中断、定时等待、公平锁)

  3. ReentrantLock(显式锁):java.util.concurrent.locks包提供的类,功能更强大
    a. 显式加锁解锁,必须手动调用lock()和unlock()
    b. 高级功能:
    ⅰ. 可中断的锁等待(lockInterruptibly())
    ⅱ. 定时锁等待(tryLock(long timeout, TimeUnit unit))
    ⅲ. 公平锁(先来后到)和非公平锁(默认,允许插队,性能更好)

  4. 读写锁:区分读操作和写操作的锁,适用于读多写少的场景
    a. ReadWriteLock
    b. StampedLock:是Java8引入的一个读写锁,它在ReentrantReadWriteLock的基础上进行了改进,提供了更灵活的读写锁机制
    ⅰ. 写锁: 独占锁,当一个线程持有写锁时,其他任何线程都不能持有读锁或写锁
    ⅱ. 悲观读锁: 共享锁,多个线程可以同时持有读锁;但当有线程持有读锁时,尝试获取写锁的线程会被阻塞
    ⅲ. 乐观读锁: 一种无条件的“读模式”。当线程尝试获取乐观读锁时,它总是立即成功,即使此时有其他线程持有写锁。然而,持有乐观读锁的线程在访问共享资源后,必须验证在它读取期间是否有写操作发生。如果没有写操作,那么它可以安全地使用读取到的数据;如果有写操作,那么它需要升级为悲观读锁或者重新读取。

Demo
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.atomic.AtomicInteger;

public class LockDemos {

// synchronized 示例
private int countSync = 0;
public synchronized void incrementSync() {
    countSync++;
}
public int getCountSync() {
    return countSync;
}

// ReentrantLock 示例
private final Lock lock = new ReentrantLock();
private int countReentrant = 0;
public void incrementReentrant() {
    lock.lock();
    try {
        countReentrant++;
    } finally {
        lock.unlock(); // 务必在 finally 中释放锁
    }
}
public int getCountReentrant() {
    return countReentrant;
}

// ReadWriteLock 示例
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private int data = 0;
public int readData() {
    readWriteLock.readLock().lock();
    try {
        System.out.println(Thread.currentThread().getName() + " 正在读取数据: " + data);
        return data;
    } finally {
        readWriteLock.readLock().unlock();
    }
}
public void writeData(int newData) {
    readWriteLock.writeLock().lock();
    try {
        System.out.println(Thread.currentThread().getName() + " 正在写入数据: " + newData);
        this.data = newData;
    } finally {
        readWriteLock.writeLock().unlock();
    }
}

// 乐观锁(CAS 实现的原子类)示例
private AtomicInteger atomicCount = new AtomicInteger(0);
public void incrementAtomic() {
    atomicCount.incrementAndGet();
}
public int getAtomicCount() {
    return atomicCount.get();
}

public static void main(String[] args) {
    LockDemos demo = new LockDemos();

    // synchronized demo
    new Thread(() -> { for (int i = 0; i < 1000; i++) demo.incrementSync(); }).start();
    new Thread(() -> { for (int i = 0; i < 1000; i++) demo.incrementSync(); }).start();
    try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    System.out.println("synchronized count: " + demo.getCountSync());

    // ReentrantLock demo
    new Thread(() -> { for (int i = 0; i < 1000; i++) demo.incrementReentrant(); }).start();
    new Thread(() -> { for (int i = 0; i < 1000; i++) demo.incrementReentrant(); }).start();
    try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    System.out.println("ReentrantLock count: " + demo.getCountReentrant());

    // ReadWriteLock demo
    new Thread(() -> demo.readData(), "Reader-1").start();
    new Thread(() -> demo.readData(), "Reader-2").start();
    new Thread(() -> demo.writeData(100), "Writer-1").start();
    new Thread(() -> demo.readData(), "Reader-3").start();

    // AtomicInteger (乐观锁) demo
    new Thread(() -> { for (int i = 0; i < 1000; i++) demo.incrementAtomic(); }).start();
    new Thread(() -> { for (int i = 0; i < 1000; i++) demo.incrementAtomic(); }).start();
    try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    System.out.println("AtomicInteger count: " + demo.getAtomicCount());
}

}

1.2)⭐死锁问题及其解决?死锁命令排查?
死锁:多线程下,两线程需要获取的资源相同,线程A获取资源1并尝试获取资源2,线程B获取资源2并尝试获取资源1,两个线程都持有锁并相互等待,形成环路

死锁排查命令:

  1. jps -l获取java进程的PID
  2. jstack -l 12345 > threaddump.log导出线程堆栈信息threaddump.log(如果有死锁,会在后面提示)
  3. 可以配合top命令,看线程的资源占用情况,如果一直不变就是死锁

解决死锁的方法:
使用资源有序分配法,即线程A、B总是以相同的顺序来获取自己想要的资源

1.3)单例模式的double-check-locking问题
在单例模式下,getInstance()的初始化逻辑应只进行一次,所以需要用synchronized关键字保证线程安全
同时,为了避免每次获取时因获取锁而造成性能损失,想出一种解决方案:double-check-locking,如下
public static SingletonDCL getInstance() {
// 第一次检查:如果实例已经存在,直接返回,无需加锁
if (instance == null) {
synchronized (SingletonDCL.class) {
// 第二次检查:在获取锁后再次检查,防止多线程创建多个实例
if (instance == null) {
instance = new SingletonDCL();
}
}
}
return instance;
}

但是,会出现问题:当执行instance = new SingletonDCL();时,如果发生指令重排
即这个语句先"为instance实例分配引用空间",后"初始化对象"
这样,第二个线程在分配完引用空间之后,判断为not null并获取对象,而这时对象是未初始化的

解决:用volatile关键字修饰instance,防止指令重排保证可见性
private static volatile SingletonDCL instance;

1.4)⭐加锁的本质是什么?
加锁的本质是让多个线程在访问共享资源时按照一定的顺序进行,或者说锁是一种线程间的协调机制,通过“互斥”和“可见性”来避免并发冲突

1.5)如何理解可重入锁?怎么实现可重入锁?
可重入锁是什么:
指同一个线程在获取了锁之后,可以再次重复获取该锁而不会造成死锁或阻塞等问题

ReentrantLock实现可重入锁的机制:基于线程持有锁的计数器
● 每次同一线程成功获取锁(不管是不是重入),都会将计数器加1
● 当这个线程释放锁时,计数器会相应地减1。只有当计数器减到0时,锁才会完全释放

synchronized实现可重入锁的机制:
synchronized底层是利用计算机系统mutex Lock实现的,每一个锁都会关联一个线程ID和一个锁状态status,这个锁状态就类似计数器

2)synchronized
2.1)⭐synchronized的三种使用方式?

  1. 修饰普通方法:
    a. 语法: 直接在普通方法的方法签名中添加synchronized关键字
    b. 锁定对象: 当一个线程调用被synchronized修饰的普通方法时,它会尝试获取当前实例对象 (this) 的锁
  2. 修饰静态方法:
    a. 语法: 在静态方法的方法签名中添加 synchronized 关键字
    b. 锁定对象: 当一个线程调用被synchronized修饰的静态方法时,它会尝试获取当前类的 Class 对象的锁(每个类在 JVM 中只有一个对应的 Class 对象)
  3. 修饰同步代码块:
    a. 语法:使用synchronized(lockObject)语句块,其中lockObject是一个任意实例对象的引用
    b. 锁定对象: 传入的实例对象

2.2)⭐synchronized的原理?锁升级机制?
在Java 15移除了偏向锁之后,synchronized的锁升级路径主要经历以下三阶段:

  1. 无锁:对象刚创建,不涉及锁竞争时
    ○ 如果第一次有线程进入synchronized代码块,才会尝试加轻量级锁
  2. 轻量级锁:乐观锁(自旋 + CAS)
    ○ 原理:CAS尝试把对象头的Mark Word改为指向锁记录的指针
    ⅰ. CAS 成功 → 当前线程独占锁(轻量级锁)
    ⅱ. CAS 失败 → 表示有竞争,自旋等待
    ○ 优点:避免进入 OS 内核态
  3. 重量级锁:悲观锁
    ○ 触发条件:多线程同时竞争,CAS多次失败,自旋超过阈值
    ○ 过程:
    ⅰ. 对象头的Mark Word会指向一个Monitor(OS级互斥量)
    ⅱ. 获取不到锁的线程会挂起(Blocked),进入 OS 等待队列
    ⅲ. 持有锁的线程释放时,OS 负责唤醒等待线程
    ○ 缺点:涉及用户态、内核态切换,上下文切换开销大

2.3)JVM对synchronized的优化?

  1. 锁膨胀:根据锁的竞争情况,逐步将锁从低开销的状态升级到高开销的状态,减少了不必要的开销
  2. 自适应自旋:
    a. 当一个线程尝试获取轻量级锁失败时,会进行自旋,即在一段时间内不断地尝试 CAS 操作获取锁
    b. 自旋的次数JVM动态调整的:如果自旋很少成功,JVM 可能会禁用自旋以避免无谓的 CPU 消耗;如果自旋经常成功,JVM 可能会适当增加自旋次数。
  3. 锁消除:
    a. VM 的即时编译器(JIT) 会对代码进行逃逸分析。如果分析发现一个对象只能被单个线程访问,那么即使代码中使用了synchronized关键字,JIT 编译器也可能会将这些锁操作消除掉,因为加锁是无意义的
  4. 锁粗化:
    a. 如果 JVM 发现一系列连续的操作都对同一个锁对象进行加锁和解锁,它可能会将这些锁的范围扩大到一个更大的粒度,以减少锁的获取和释放次数,提高性能

3)Reentrantlock
3.1)Reentrantlock的底层原理?
ReentrantLock 的底层是基于 AQS(AbstractQueuedSynchronizer) 实现的。它内部维护了一个 state 状态值和一个 等待队列。
当线程调用 lock():
● 会用 CAS 尝试把 state 从 0 改成 1,成功就获得锁;
● 如果失败,说明别的线程占着锁,它就会进入 AQS 队列 挂起等待(用 LockSupport.park())。
当持锁线程调用 unlock():
● 会让 state 减 1;
● 如果变成 0,说明锁完全释放了,就会唤醒队列里的下一个等待线程。
因为同一个线程加锁时会检测自己是不是持有者,如果是,就简单地 把 state +1,所以叫“可重入锁”。

3.2)为什么非公平锁吞吐量大?
公平锁的不足:
线程在获取公平锁时,即使锁是空闲的,也需要先检查等待队列是否有等待时间长的线程并唤醒,这些检查和维护队列的操作会引入额外的开销

非公平锁:
非公平锁允许线程在尝试获取锁时直接尝试抢占,这样"先尝试后等待"的机制减少了频繁的线程上下文切换带来的开销,提高了cpu的利用率

3.3)⭐ReentrantLock公平锁和非公平锁有什么区别?
公平锁:
● 定义:按照线程请求锁的先后顺序(FIFO)来获取锁
● 特点:谁先等待,谁先拿到锁 → 避免线程“饥饿”
● 缺点:性能相对较低,因为要维护等待队列,频繁切换上下文

非公平锁:
● 定义:线程请求锁时,直接尝试获取,如果失败才进入队列
● 特点:可能“插队”,导致某些线程长时间拿不到锁(饥饿),但吞吐量更高

4)对比
4.1)⭐⭐synchronized和ReentrantLock的区别?
配合6.讲

  1. 底层实现不一样:
    a. synchronized是JVM内置关键字,依赖于Monitor对象实现
    b. ReentrantLock是 JDK 层面(API层面)实现的
  2. ReentrantLock比synchronized增加了一些高级功能
    a. ReentrantLock增加了一些高级功能
    ⅰ. 等待可中断
    ⅱ. 定时锁等待
    ⅲ. 公平锁
    ⅳ. 条件变量
  3. 锁的获取与释放
    a. synchronized:隐式地获取和释放锁,进入同步块自动获取,退出自动释放
    b. ReentrantLock:需要显式地使用 lock() 方法获取锁,unlock()方法释放锁

4.2)synchronized和ReentrantLock的应用场景?
配合5.讲
synchronized:
● 简单且竞争不激烈的场景: synchronized 通常是一个简单且高效的选择,代码也更易读
● 常作为实现其他并发工具的基础: ConcurrentHashMap

ReentrantLock:
● 高级锁功能需求: 如可中断、锁超时、公平锁、条件变量
● 性能优化: 在高度竞争的环境中,ReentrantLock可以提供比synchronized更好的性能,因为它的高级功能提供了更细粒度的控制,如尝试锁定和定时锁定,可以减少线程阻塞的可能性
条件变量就是在线程需要等待某个条件成立时,把线程挂起,等条件满足再唤醒

6)CountDownLatch是什么?
核心就是一个计时器,使用流程:
● 初始化计数器:创建CountDownLatch时指定一个初始计数值(如 N)
● 等待线程阻塞:调用await()的线程会被阻塞,直到计数器变为 0,所有等待的线程会被唤醒
● 任务线程通知:其他线程完成任务后调用 countDown(),使计数器减 1

场景:
● 主线程等待多个子线程完成初始化任务: 如,一个应用启动时需要加载多个模块的数据,可以把每个模块的数据加载工作交给一个独立的子线程。主线程可以使用 CountDownLatch 等待所有模块的数据加载完成后再继续执行后续的操作。
● 多个线程等待某个特定事件发生后同时开始执行: 如,在进行性能测试时,可能需要多个并发线程同时开始执行压力测试。可以初始化一个大小为n的CountDownLatch,当准备工作完成后,所有线程调用 countDown() 方法,然后等待在await()上的测试线程可以同时开始执行。

7)ThreadLocal
7.1)⭐讲讲ThreadLocal的工作原理?
ThreadLocal是什么:
● 作用:为每个线程提供变量的独立副本,线程之间互不影响
● 核心点:每个线程内部维护一个ThreadLocalMap:
○ key= ThreadLocal对象
○ value= 该线程自己的变量副本

使用场景:
● 请求上下文传递(Web/分布式应用): 如SpringSecurity的SecurityContextHolder保存用户权限信息
● 避免参数层层传递:不需要把数据显式传给每个方法

7.2)⭐讲讲ThreadLocal内存泄漏问题?
ThreadLocalMap的元素为Entry,这个Entry的key被定义为弱引用(每次GC时回收),而value是强引用
当弱引用key被GC回收后,强引用value还是被线程持有(引用),导致一直占用内存(内存泄漏)
解决方法:用try-finally机制,在set一个Entry并业务处理后,remove这个Entry,手动清理内存

三、线程池相关

1)⭐ThreadPoolExecutor构造参数有哪些?

  1. corePoolSize:核心线程数,默认情况下空闲状态不会销毁
    a. 核心线程数可以为0,这时所有线程都是非核心线程
  2. maximumPoolSize:最大线程数,新任务提交且线程池没有空闲线程,会创建新线程来执行新任务
  3. keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被销毁
  4. workQueue:任务等待队列
  5. threadFactory:线程工厂,可以用来给线程取名
  6. handler:拒绝策略

2)⭐线程池的执行流程是什么?
假设调用executor.execute(task)提交任务:

  1. 核心线程是否已满?
    ● 未满 → 创建新线程执行任务
    ● 已满 → 进入下一步
  2. 任务能否进入队列?
    ● 能进入 → 放入队列等待
    ● 不能进入(队列满)→ 进入下一步
  3. 线程数是否达到maximumPoolSize?
    ● 未达到 → 创建新线程执行任务
    ● 已达到 → 进入下一步
  4. 执行拒绝策略
    ● 由RejectedExecutionHandler决定如何处理任务

3)⭐有几种拒绝策略?

  1. AbortPolicy:默认的拒绝策略,直接抛异常

  2. CallerRunsPolicy:被拒绝的任务会被提交任务的线程自己执行

  3. DiscardPolicy:直接抛弃任务,不做任何处理

  4. DiscardOldestPolicy: 丢弃工作队列中等待时间最长的任务,然后尝试重新提交当前被拒绝的新任务

  5. 自定义拒绝策略:重写RejectedExecutionHandler.rejectedExecution
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2),

         // 匿名内部类实现 RejectedExecutionHandler 接口
         new RejectedExecutionHandler() {
             @Override
             public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                 System.out.println("--------------------------------------------------");
                 System.out.println("匿名内部类拒绝策略:任务 [" + r.toString() + "] 被拒绝!");
                 System.out.println("  - 活跃线程数: " + executor.getActiveCount());
                 System.out.println("  - 任务队列大小: " + executor.getQueue().size());
                 // 这里同样可以添加自定义的日志、告警或任务持久化逻辑
                 System.out.println("--------------------------------------------------");
             }
         }
    
     );
    

4)⭐核心线程数的最佳实践?
根据任务类型选择合适的算法
● CPU密集型任务: 任务的主要瓶颈在于CPU的计算能力(如大量数据排序、复杂数学运算等),线程大部分时间都在执行计算,很少等待
○ 线程数建议: 设置为CPU核心数 N
○ 原因: 过多的线程会导致频繁的上下文切换,会降低 CPU 利用率。N+1的说法已不再推荐,因为即使预留一个线程也无法凭空增加 CPU 处理能力
● I/O密集型任务:任务大部分时间在等待 I/O 操作(如网络请求、文件读写),CPU 利用率相对较低
○ 线程数建议: 设置为M * N,其中 N 是 CPU 核心数,M通常建议默认设置为 2
○ 原因: 当一个线程等待 I/O 时,它不占用CPU,可以创建更多线程来填充 I/O等待空隙,以充分利用 CPU 资源。M 的具体取值需要根据实际的 I/O 等待时间和任务特性进行测试和调优
解释之前CPU密集型任务:为什么N+1
缺页中断 (Page Fault): 当线程访问的内存页不在物理内存中时,会触发缺页中断,导致线程暂时阻塞,等待操作系统将所需页从磁盘加载到内存

5)⭐线程池种类有哪些?
一般讨论的是Executor框架的工具类Executors的一些构造方法(这些类可以直接new)

  1. FixedThreadPool:固定线程数量的线程池
    a. 核心线程数与最大线程数相等,使用无界队列
  2. SingleThreadExecutor:只有一个线程的线程池,保证所有任务都在同一个线程中按提交顺序执行
    a. 使用无界队列
  3. CachedThreadPool:线程数量不确定
    a. 核心线程数:0,最大线程数:Integer.MAX_VALUE
  4. ScheduledThreadPool:延迟或定期运行任务
    a. 最大线程数:Integer.MAX_VALUE
    b. 使用延迟队列,只有任务到执行时间才会被执行

6)为什么不建议用Executors工具类?
《阿里巴巴 Java 开发手册》中提到,禁止使用这些方法来创建线程池

  1. 资源耗尽风险:FixedThreadPool和SingleThreadExecutor的无界队列, 队列会无限增长;CachedThreadPool 的无限线程数,无限制地创建新线程,最终可能导致OOM
  2. 缺乏细粒度的控制:Executors提供的工厂方法隐藏了ThreadPoolExecutor的许多关键参数(如核心线程数、最大线程数、任务队列、线程存活时间、拒绝策略等),这意味着你无法根据应用的具体需求和系统资源情况进行细粒度的配置。

7)线程池中shutdown(), shutdownNow()方法的区别

  1. shutdown()
    ● 不再接收新任务
    ● 已提交的任务:
    ○ 正在执行的 → 继续执行完。
    ○ 在等待队列里的 → 继续执行。
    ● 线程池最终会在任务执行完毕后关闭

  2. shutdownNow()
    ● 不再接收新任务
    ● 尝试中断正在执行的任务:调用线程的interrupt()方法
    ○ 如果任务代码里没有响应中断(例如没有用 Thread.sleep()、wait()、BlockingQueue.take() 等),那么任务可能仍然继续执行
    ● 队列中尚未执行的任务:会直接返回一个List,表示这些任务未被执行

8)提交给线程池的任务可以被撤回吗?
当向线程池提交任务时,会得到一个Future对象。这个Future对象提供了几种方法来管理任务的执行,包括取消任务

取消任务的主要方法是Future接口中的cancel(boolean mayInterruptIfRunning)方法。参数mayInterruptIfRunning指示是否允许中断正在执行的任务。如果设置为true,则表示允许中断已开始执行的任务;如果设置为false,任务已经开始执行则不会被中断
public interface Future {
// 是否取消线程的执行
boolean cancel(boolean mayInterruptIfRunning);
}

9)⭐execute()和submit()有什么区别?
方法 所属接口 返回值 接受任务类型
execute(Runnable task) Executor void Runnable
submit(Runnable/Callable task) ExecutorService Future<?> Runnable/Callable

  1. execute():无返回值,异常会直接抛出到线程池的处理器(手写捕获逻辑,或默认打印在控制台)
  2. submit():适合需要获取任务结果的情况,异常会封装到返回的Future对象

四、场景

1)⭐多线程按顺序打印奇偶数
法一:用锁保证线程交替执行,wait()、notify()用于线程协作
思路:线程获取锁,检查是否应该打印,然后wait()释放锁并等待被notify()打断
public class PrintOddEven {
// 锁
private static final Object lock = new Object();
// 共享的计数器
private static int count = 1;
private static final int MAX_COUNT = 10;

public static void main(String[] args) {
    Runnable printOdd = () -> {
        synchronized (lock) {
            while (count <= MAX_COUNT) {
                if (count % 2 != 0) {
                    System.out.println(Thread.currentThread().getName() + ": " + count++);
                    lock.notify();
                } else {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };

    Runnable printEven = () -> {
        synchronized (lock) {
            while (count <= MAX_COUNT) {
                if (count % 2 == 0) {
                    System.out.println(Thread.currentThread().getName() + ": " + count++);
                    lock.notify();
                } else {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };

    Thread oddThread = new Thread(printOdd, "OddThread");
    Thread evenThread = new Thread(printEven, "EvenThread");

    oddThread.start();
    evenThread.start();
}

}

2)n个线程并发执行,主线程等待这些线程执行完再执行
思路:
使用 CountDownLatch 来实现 3 个线程并发执行,另一个线程等待这三个线程全部执行完再执行的需求

实现步骤:
● 创建一个 CountDownLatch 对象,并将计数器初始化为 3,因为有 3 个线程需要等待
● 创建 3 个并发执行的线程,在每个线程的任务结束时调用countDown方法将计数器减 1
● 创建第 4 个线程,使用 await 方法等待计数器为 0,即等待其他 3 个线程完成任务

3)⭐高并发下的优化方案?

  1. 架构层优化
    ● 分布式服务:负载均衡(配合Nginx、网关)
    ● 多级缓存:本地缓存、分布式缓存
    ● 消息队列:用于异步削峰填谷
    ● 限流与熔断:Sentinel,防止流量打爆下游服务

  2. 数据库优化
    ● 索引优化:建合适的索引,避免全表扫描
    ● 主从读写分离:主库写,从库读,减轻主库压力
    ● 分库分表:解决单库单表的性能瓶颈

  3. 应用层优化
    ● 线程池:并发处理任务,设置合理参数
    ● 锁优化:缩小锁粒度(分段锁、读写锁)

  4. JVM优化
    ● GC调优:选择合适的垃圾回收器(G1、ZGC、Shenandoah)
    ● 内存分配:调整堆大小,减少 Full GC

4)⭐如果要执行一批任务,怎么确定用多进程/多线程/虚拟线程(协程)
模型 适用场景
多进程 计算量大、任务独立可拆分服务
多线程 CPU 密集型任务,可以使用线程池
虚拟线程/协程 IO 密集型任务,如网络请求高并发处理、RPC调用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值