【JAVA基础面经】juc包(java.util.concurrent)

文章目录


前言

JUC 提供了比传统 synchronizedwait/notify 更灵活、更高效的并发工具。

一.Callable

对于传统的 Runnable 接口来说,任务执行完毕后,主线程无法直接获取计算结果,且无法抛出受检异常(run()方法签名未声明 throws,导致异常只能内部消化)。

1.Callable的实现

Callable 接口则可以解决 Runnable 带来的问题,Callable 是一个泛型接口,定义如下:

java 复制代码
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
java 复制代码
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo {
    public static void main(String[] args) {
        //通过Callable来描述一个任务,泛型参数表示返回值的类型
        Callable<Integer> callable = new Callable<Integer>() {
            //重写callable中的call方法
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for(int i=0;i<=1000;i++){
                    sum += i;
                }
                return sum;
            }
        };
        //为了让线程执行 callable 中的任务,需要创建一个辅助的类对Callable进行封装,线程通过该类获取任务
        FutureTask<Integer> task = new FutureTask<Integer>(callable);
        //创建线程,用来执行任务
        Thread t =new Thread(task);
        t.start();
        //如果线程任务没有执行完成,get就会陷入阻塞
        //会一直阻塞到任务完成,得出计算结果
        try {
            System.out.println(task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
  • call() 方法声明了 throws IOException,因此任务内部可以直接 throw 受检异常,无需 try-catch 包裹。
  • 调用 FutureTask.get() 时,若任务抛出异常,get() 方法会将原始异常包装为 ExecutionException 抛出。

2.Future 接口

Future 是一个泛型接口,代表异步计算的结果

主线程将任务交给子线程后,可以得到一个Future。主线程可以先做别的事,需要时凭借Future的 get() 即可拿到结果,若线程还没完成则原地等待。

方法 描述
boolean cancel(boolean mayInterruptIfRunning) 尝试取消任务执行。
boolean isCancelled() 判断任务是否已被取消。
boolean isDone() 判断任务是否执行完成(正常结束、异常结束或取消均返回 true)。
V get() 阻塞获取计算结果,直到任务执行完毕。
V get(long timeout, TimeUnit unit) 带超时时间的阻塞获取,超时抛出 TimeoutException。

3.FutureTask

FutureTask 是 Future 接口的唯一实现类,同时它也实现了 RunnableFuture 接口(该接口继承自 Runnable 和 Future)。

  • Runnable 身份:作为 Thread 构造函数的参数,负责执行 Callable 中的任务逻辑。
  • Future 身份:作为调用方获取结果的句柄,提供 get()、cancel() 等方法。

FutureTask 是连接 Callable 任务与 Thread 线程的桥梁,同时承担了存储异步计算结果与异常的重任。

二.ReentrantLock

ReentrantLock 是 Java 并发包中提供的一把显式锁,需要开发者手动控制加锁与解锁

1.ReentrantLock的使用

  • lock.lock():解释
  • lock.unlock():解释
java 复制代码
ReentrantLock lock = new ReentrantLock();

// 加锁
lock.lock();
try {
    // 受保护的临界区代码
    // 例如:对共享变量进行写操作
} finally {
    // 务必在 finally 块中释放锁,避免因异常导致死锁
    lock.unlock();
}

2.ReentrantLock 与 Synchronized 对比

对比维度 synchronized 关键字 ReentrantLock 类
实现层面 JVM 内置关键字,由 C++ 实现 JDK 层面的 Java 类,基于 AQS 框架实现
锁的释放 自动释放:代码块执行完毕或抛出异常时,JVM 自动释放锁 手动释放:必须在 finally 中显式调用 unlock()
公平策略 仅支持非公平锁 构造函数支持选择两种锁类型,new ReentrantLock(true) 可创建公平锁
等待可中断 线程在等待锁期间只能被动死等,无法被外部打断。一旦陷入阻塞,除非成功获取锁,否则线程将一直处于 BLOCKED 状态,无法响应中断信号 提供了 lockInterruptibly() 方法,支持响应中断。当线程 A 在等待锁时,若其他线程调用了 A.interrupt(),线程 A 会立刻从阻塞中醒来,并抛出 InterruptedException,从而让上层代码有机会处理取消逻辑
尝试获取锁 一旦锁被占用,当前线程别只能进入阻塞队列无限期等待,直到锁被释放 tryLock():一次性的尝试 。若锁空闲则立即获取并返回 true;若锁被占用则立即返回 false,线程不会阻塞,可转而执行其他备选逻辑 tryLock(long time, TimeUnit unit):带超时的尝试。在指定时间内反复尝试获取锁,超时未获取则自动放弃并返回 false
条件队列 单一 wait/notify/notifyAll 机制 支持多个 Condition 条件队列,可精确唤醒特定类型的等待线程
性能表现 JDK 1.6 优化后,基础场景下两者性能相近 在复杂同步策略或高竞争场景下功能更灵活

4.Condition(非juc包)

Condition 不是juc包下的,而是ReentrantLock 的衍生?

Condition.await():

Condition.signal():

代码例子

java 复制代码

3.AQS抽象类

AQS(AbstractQueuedSynchronizer)是Java中的一个抽象类。 AQS是一个用于构建锁、同步器、协作工具类的工具类,是 JUC 包的基石,是一个用于构建锁、同步器和协作工具类的抽象框架。

ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等并发工具的内部同步器均基于 AQS 实现。

AQS 通过两大核心组件来管理线程的同步状态:

(1)同步状态 ------ volatile int state(被 volatile 修饰的整型变量,用来标识资源的状态)

  • 作用:表示锁的持有情况。
  • 操作:通过 getState()、setState() 以及 CAS 来安全修改状态值。
  • 语义映射(以 ReentrantLock 为例):
    • state == 0:锁空闲。
    • state >= 1:锁已被线程持有。若 state > 1,表示该线程发生了重入。

(2)等待队列 ------ CLH 变体队列

  • 数据结构:一个虚拟的 FIFO 双向链表。
  • 节点类型:内部类 Node,封装了等待线程的引用、等待状态(waitStatus)以及前后指针。
  • 工作机制:
    • 入队:当线程尝试获取锁失败时,会被包装成 Node 节点,通过 CAS 操作原子性地加入到队列尾部,随后通过 LockSupport.park() 挂起。
    • 出队:当锁被释放时(state 置 0),AQS 会唤醒队列的头节点后继(通常是第一个有效等待节点),被唤醒的节点再次尝试 CAS 获取锁。


图片来源

4.ReentrantLock 基于 AQS 的实现

【补图】

三.原子类 (Atomic 类)

原子类的内部基于CAS机制实现,性能比加锁实现 i++ 高很多,常用的原子类有如下几个:

类名 说明
AtomicBoolean 布尔型原子类
AtomicInteger 整型原子类
AtomicLong 长整型原子类
AtomicIntegerArray 长整型原子类
AtomicLong 整型数组原子类
AtomicReference 引用类型原子类
AtomicStampedReference 原子更新带有版本号的引用类型

以AtomicInteger举例,常见的方法有

java 复制代码
addAndGet(int delta);      i += delta
decrementAndGet();   --i
getAndDecrement();   i--
incrementAndGet();  ++i
getAndIncrement();   i++

四.线程池

1.线程池的创建

(1)ExecutorService 和 Executors

ExecutorService 表示一个线程实例,Executors是一个工厂类,能给个创建出几种不同风格的线程池。通过ExecutorService 中的 submit 方法能够向线程池中提交若干个任务

java 复制代码
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
    }
});

Executors创建线程的几种方式

方法 说明
newFixedThreadPool 创建固定线程数的线程池
newCachedThreadPool 创建线程数目动态增长的线程池
newSingleThreadExecutor 创建只包含单个线程的线程池
newScheduledThreadPool 设定延迟时间后执行命令,或定期执行命令,是进阶版的Timer

(2)ThreadPoolExecutor

Executors本质上是对ThreadPoolExecutor的封装,ThreadPoolExecutor提供了更多的可选参数,库进一步细化线程池行为的设定,其构造方法如下。

手动创建 ThreadPoolExecutor 需要指定 7 个核心参数

java 复制代码
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • int corePoolSize 表示核心线程数
  • int maximumPoolSize 表示最大线程数,包括核心线程和非核心线程。
  • long keepAliveTime 表述了非核心线程的存活时间
  • TimeUnit unit 表示keepAliveTime的时间单位
  • BlockingQueue workQueue 任务队列,线程池会提供一个submit方法将任务放入到线程池任务队列中
  • ThreadFactory threadFactory 线程工厂,描述线程是怎么创建出来的
  • RejectedExecutionHandler handler,拒绝策略,当任务队列满了设定的 具体策略,包括忽略最新任务、阻塞等待、丢弃最旧任务等方式

(3)四种常见拒绝策略

策略类 行为描述
AbortPolicy (默认) 直接抛出 RejectedExecutionException 异常,阻断系统运行。
CallerRunsPolicy 由调用者线程(提交任务的线程)执行该任务,起到流量削峰作用。
DiscardPolicy 直接丢弃新任务,无任何通知(极度危险,慎用)。
DiscardOldestPolicy 丢弃队列中存活最久的任务,然后尝试重新提交新任务。

2.线程池的原理与使用

线程池包含核心线程数、最大线程数以及一个任务等待队列。

  • 核心线程数 (corePoolSize):除非系统空闲回收,否则始终驻留的线程,用于快速响应常态任务量。
  • 最大线程数 (maximumPoolSize):线程池扩容的极限值,用于应对突发峰值流量。
  • 任务等待队列 (workQueue):起到缓冲池的作用,当核心线程繁忙时,任务先进入队列排队等待。

下面的代码基于 ThreadPoolExecutor 手动创建

java 复制代码
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 1. 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,                                          // corePoolSize 核心线程数
                5,                                          // maximumPoolSize 最大线程数
                60,                                         // keepAliveTime 空闲存活时间
                TimeUnit.SECONDS,                           // 时间单位
                new ArrayBlockingQueue<>(3),                // 有界任务队列,容量为 3
                Executors.defaultThreadFactory(),           // 默认线程工厂
                new ThreadPoolExecutor.CallerRunsPolicy()   // 拒绝策略:由调用线程执行
        );

        // 2. 提交任务
        for (int i = 0; i < 10; i++) {
            int taskId = i;
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " 正在执行任务: " + taskId);
                try {
                    Thread.sleep(2000); // 模拟业务耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 3. 关闭线程池(不再接收新任务,等待已提交任务执行完毕)
        executor.shutdown();
    }
}

3.线程池的参数设置

1)CPU 密集型任务

  • 主要消耗 CPU 资源进行计算,几乎无阻塞。
  • 核心线程数 = CPU 核心数 + 1。
  • 防止过多线程导致频繁上下文切换,加 1 是为了利用因缺页中断或其他原因暂停的空隙。

(2)IO 密集型任务

  • 频繁进行网络或磁盘读写,线程大部分时间处于阻塞等待状态。
  • 核心线程数 = CPU 核心数 * 2 或使用公式 CPU 核心数 / (1 - 阻塞系数)。
  • 阻塞期间线程不占用 CPU,可多开线程利用 CPU 空闲时间处理其他任务。

(3)通用估算公式

  • 最佳线程数 = CPU 核心数 × 目标 CPU 利用率 × (1 + 平均等待时间 / 平均计算时间)

五、同步工具类

1.CountDownLatch

2.CyclicBarrier

3.Semaphore

六、线程安全的集合类

七.面试问题

1.线程获取不到锁会直接进入到阻塞队列吗?

  • synchronized:会先进行自旋等待(默认循环 10 次),自旋期间不进入阻塞队列,如果自旋期间拿到了锁,就继续执行;如果自旋失败或竞争加剧,才会膨胀为重量级锁,此时才会调用操作系统 mutex,线程真正进入阻塞队列(等待队列),状态变为 BLOCKED。
  • ReentrantLock:

2.CAS 和 AQS 有什么关系?

1.为什么要使用线程池,线程池的作用

2.核心线程数设置为0可不可以?

3.线程池用了哪些设计模式?

4.shutdown和shutdownNow的区别

java 复制代码
public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}
java 复制代码
public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

5.提交给线程池中的任务可以被撤回吗?

相关推荐
迷藏4942 小时前
**RISC-V生态下的嵌入式开发新范式:从指令集到自定义外设的全流程实战**在当前国产化
java·python·risc-v
小松加哲2 小时前
Tomcat 核心原理全解析(含请求流转+组件源码+多应用配置)
java·tomcat·firefox
‎ദ്ദിᵔ.˛.ᵔ₎2 小时前
C++ 继承
开发语言·c++
殇淋狱陌2 小时前
【初始Python】Python学习基础(数据类型、定义、变量、下标、目前的开发语言对比)
开发语言·python·学习
色空大师2 小时前
【nacos下载安装】
java·linux·nacos·ubantu
lsx2024062 小时前
Ruby 迭代器
开发语言
朱一头zcy2 小时前
Java基础复习08:IO流(File类与IO流概述、字节输入输出流、字符输入输出流、缓冲流、字符转换流、对象序列化、打印流、Commons-io包介绍)
java·笔记
一叶飘零_sweeeet2 小时前
击穿 Java 高并发性能瓶颈:伪共享底层原理、缓存行填充与 @Contended 注解全维度深度拆解
java·伪共享
史迪仔01122 小时前
[QML] Popup 与 Dialog
开发语言·前端·javascript·c++·qt