当前位置: 首页 > article >正文

20250121面试鸭特训营第29天

更多特训营笔记详见个人主页【面试鸭特训营】专栏

250121

1. 你了解 Java 线程池的原理吗?

  • 线程池是一种池化技术,用于预先创建并管理一组线程,避免频繁创建和销毁线程的开销,提高性能和响应速度。

线程池的 7 个参数

  • corePoolSize
    • 核心线程数,线程池中始终存活的线程数,即使它们是空闲的 。
  • maximumPoolSize
    • 最大线程数,线程池中允许的最大线程数。
  • keepAliveTime
    • 线程空闲时间,当线程数大于核心线程数时,空闲线程在等待新任务到达的最大时间 ,超过这个时间的非核心线程会被销毁。
  • unit
    • keepAliveTime的时间单位,可以是天、小时、分钟、秒等 。
  • workQueue
    • 一个阻塞队列,用来存储线程池等待执行的任务 。
    • 工作队列的类型有
      • SynchronousQueue:不存储任务,直接将任务提交给线程。
      • LinkedBlockingQueue:链表结构的阻塞队列,大小无限。
      • ArrayBlockingQueue:数据结构的有界阻塞队列。
      • PriorityBlockingnQueue:带优先级的无界阻塞队列。
  • threadFactory
    • 线程工厂,主要用来创建线程,默认为正常优先级、非守护线程 。
  • handler
    • 拒绝策略,当任务过多(处理不过来)时提供的策略 。

线程池的工作原理

  • 首先使用核心线程来执行任务 。

在这里插入图片描述

  • 如果核心线程都忙,且任务队列未满,任务会被放入任务队列中 。

在这里插入图片描述

  • 如果核心线程都忙,且任务队列已满,且线程数小于最大线程数,线程池会创建非核心线程来处理任务 。

在这里插入图片描述

  • 如果核心线程都忙,且任务队列已满,如果线程数已经达到最大线程数,线程池会采取拒绝策略来处理新提交的任务 。

在这里插入图片描述

  • 如果线程空闲时间超过空闲存活时间,并且当前线程数大于核心线程数,则会销毁线程,直到线程数等于核心线程数。( allowCoreThreadTimeOut 默认为 false,设置为 true 可以回收核心线程)

4种拒绝策略

  • AbortPolicy:丢弃任务,并抛出 RejectedExecutionException 异常
  • CallerRunsPolicy:由调用线程(提交任务的线程)来处理该任务
  • DiscardPolicy:直接丢弃无法处理的任务,不进行任何特殊处理
  • DiscardOldestPolicy:丢弃队列中等待最久的任务,然后尝试再次提交当前任务

为什么先用阻塞队列而不是直接增加线程

  • 因为创建线程需要占用一定系统资源(栈空间、线程调度开销等),导致性能下降。
  • 使用阻塞队列可能将任务暂存,避免直接增加线程数带来的资源消耗。
  • 如果阻塞队列都满了,说明系统负荷太大了,这时候再增加线程到最大线程数去消化任务。
  • 举例
    • 老板有 10 个员工(10个核心线程数),现在 10 个人手里都有活在干(10 个线程都有任务正在执行)。
    • 这时候如果又来了 5 个活(5个任务),老板肯定不会立马再招 5 个人(新建线程)来干活,而是积攒着这些活(把任务放入阻塞队列)。
    • 但是如果老板发现活积累的实在太多了(阻塞队列满了),才会继续招人干活(直接招满,相当于达到最大线程数)。

2. 你使用过哪些 Java 并发工具类?

ConcurrentHashMap

  • 线程安全的哈希表,提供了高效的并发操作。
  • 使用分段锁机制,多个线程可以同时访问不同的段,提高了并发性能。
  • 提供了类似于 Map 的基本操作,如 getputremove,同时还支持并发修改。
  • 适用于读多写少的场景,可以提供高并发的读写性能。
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.putIfAbsent("key2", 2);  // 如果key2不存在,才会放入
map.putIfAbsent("key2", 3);  // 由于key2已存在,不会放入
int value1 = map.get("key1");
System.out.println("value1 = " + value1); // 打印value1 = 1
int value2 = map.get("key2");
System.out.println("value2 = " + value2); // 打印value2 = 2

AtomicInteger

  • 是一个线程安全的整数类,用于对 int 类型进行原子性操作(如加减、比较、交换),保证了操作的线程安全性。
  • 使用 CAS 机制,避免了加锁。
  • 适用于需要频繁修改整数的无锁场景,例如计数器、标志位等。
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 自增1
count.decrementAndGet(); // 自减1
count.addAndGet(5); // 加5
int num = count.get(); // 获取当前值
System.out.println("num = " + num); // 打印num = 5

Semaphore

  • 是一种信号量,在资源有限的情况下, 控制同一时刻访问共享资源的线程数量。
    • 信号量的计数器表示可用的资源数量,线程获取资源时计数器减一,释放资源时计数器加一。
    • 如果计数器为零,线程会被阻塞直到有资源可用。
  • 通过计数器来控制访问资源的线程数量,适用于限制并发访问资源的场景。
Semaphore semaphore = new Semaphore(3);  // 允许最多3个线程并发访问
try {
    semaphore.acquire();  // 获取许可
    // 执行任务
} finally {
    semaphore.release();  // 释放许可
}

CyclicBarrier

  • 是一个同步工具类,它使得一组线程可以相互等待,直到所有线程都到达某个时间点(例如等待其他线程完成某个阶段),然后一起继续执行。
  • 适用于需要所有线程在某个时间点都完成后再继续的场景。
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程到达屏障点")
});
Rannable task = () -> {
    try {
        // 执行任务
        barrier.await();  // 等待其他线程
    } catch (Exception e){
        e.printStackTrace();
    }
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();

CountDownLatch

  • 是一个同步工具类,用于使一个或多个线程等待,直到其他线程全都完成操作。
  • 适用于主线程需要等待多个子线程完成任务的场景。
CountDownLatch latch = new CountDownLatch(3);
Runnable task = () -> {
    try {
        // 执行任务
    } finally {
        latch.countDown(); // 任务完成,计数器减一
    }
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
latch.await();  // 等待计数器减到0,即等待所有任务都完成
System.out.println("所有任务都完成了");

BlockingQueue

  • 是一个支持线程安全的队列,它可以在队列为空时阻塞取元素操作,在队列满时阻塞插入元素操作,适用于生产者-消费者模式。
  • 生产者线程将元素放入队列,消费者线程从队列中取元素,队列为空时消费者线程阻塞,队列为满时生产者线程阻塞。
BlockingQueue<String> queue = new ArrayBlockingQueue<>();

// 生产者线程
Runnable producer = () -> {
    try {
        queue.put("item");  // 放入元素,如果队列满,则阻塞
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

// 消费者线程
Runnable consumer = () -> {
    try {
        String item = queue.take();  // 取出元素,如果队列为空,则阻塞
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

new Thread(producer).start();
new Thread(consumer).start();

3. 什么是 Java 的 CAS(Compare-And-Swap)操作?

基本概念

  • CAS 是一种硬件级别的原子操作,主要有三个参数,待更新的内存地址、期望值、新值。
  • CAS 比较内存中的某个值是否为预期值,如果是,则更新为新值,否则不做修改。
  • 比较(Compare):CAS 会检查内存中的某个值是否与预期值相等。
  • 交换(Swap):如果相等,则将内存中的值更新为新值。
  • 失败重试:如果不相等,说明有其他线程已经修改了该值。CAS 操作失败,一般会重试直到成功。

举例说明

int a = 0;
// a++操作在多线程环境下是不安全的
a++; // 分为三步,1.取a的值  2.a+1  3.把新值写回a
System.out.println("a == " + a);

在这里插入图片描述

  • 这种情况加锁是可以解决的,但是加锁过于消耗资源。
  • CAS 操作的解决方案
    • CAS 的三个参数:内存地址(a的内存地址)、期望值(1)、新值(2)。
    • 线程 A 通过 a 的内存地址找到 a ,比较 a 的当前值 1 与期望值 1 是否相同。
    • 若相同,则线程 A 将 a 的值更新为新值 2 。
    • 若不相同,说明 a 已经被其他线程修改,需要重新尝试修改。

CAS 的优缺点

  • 优点
    • 无锁并发:CAS 操作不使用锁,因此不会导致线程阻塞,提高了系统的并发性和性能。
    • 原子性:CAS 操作是原子的,保证了线程安全。
  • 缺点
    • ABA 问题:CAS 操作中,如果一个变量值从 A 变成 B 又变回 A,CAS 无法检测到这种情况,可能导致错误。
    • 自旋开销:CAS 操作通常通过自旋实现,可能导致 CPU 资源浪费,尤其是在高并发情况下。
    • 单变量限制:CAS 操作仅适用于单个变量的更新,不适用于涉及多个变量的复杂操作。

http://www.kler.cn/a/516715.html

相关文章:

  • 当 Facebook 窥探隐私:用户的数字权利如何捍卫?
  • PHP礼品兑换系统小程序
  • (长期更新)《零基础入门 ArcGIS(ArcMap) 》实验六----流域综合处理(超超超详细!!!)
  • 网络安全等级保护基本要求——等保二级
  • MySQL(4)多表查询
  • 嵌入式硬件篇---ADC模拟-数字转换
  • Python的进程和线程
  • stm32f103 单片机(一)第一个工程
  • 2025.1.21——六、BUU XSS COURSE 1 XSS漏洞|XSS平台搭建
  • react引入DingTalk-JinBuTi字体
  • 考研机试题:打印数字菱形
  • 宝塔Linux面板教程
  • 【数据结构】深入解析:构建父子节点树形数据结构并返回前端
  • TCP 详解
  • 【uniapp】获取上传视频的md5,适用于APP和H5
  • linux实时流量监控工具iftop详解
  • 【优选算法】8----四数之和
  • 网络安全 | 0day漏洞介绍
  • iOS 集成ffmpeg
  • 深度剖析:AI Agent 与 RPA 融合的底层技术逻辑
  • python 组播udp诊断
  • 解锁C#编程新姿势:Z.ExtensionMethods入门秘籍
  • MySQL用户授权、收回权限与查看权限
  • AI知识库如何提升电子电器企业的运营效率
  • MVCC在MySQL中实现无锁的原理
  • C语言基础------练习