尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

JUC并发编程知识三(待完善)

JUC并发编程知识三(待完善)
📅 发布时间:2026/7/5 20:43:11

ThreadLocal

1.ThreadLocal是什么?

ThreadLocal是Java用于实现“线程本地存储变量”的工具类。他的作用是:为每个线程创建独有的变量副本,线程只能读写自己的副本,所以不存在多线程竞争资源问题,更不需要加锁来实现线程安全。


2.为什么要使用ThreadLocal

同一份数据被多线程同时访问会出现线程安全问题,通常解决方案是加锁,但加锁会造成阻塞、会降低并发性能。而ThreadLocal 天然线程安全,不需要加锁也能够提升并发性能。


3.ThreadLocal如何使用?

标准使用步骤(3 步)

1. 定义ThreadLocal实例(全局唯一,通常用static修饰)

方式1:初始值为 null private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); 方式2:用 withInitial 指定初始值(JDK8+) private static ThreadLocal<Integer> numLocal = ThreadLocal.withInitial(() -> 0);

2. 在任意线程中,通过 set方法设置当前线程的副本 \ 通过get方法获取自己的副本

线程1中设置值 new Thread(() -> { threadLocal.set("线程1的私有数据"); // 为当前线程(线程1)创建副本并赋值 // ... 其他操作 String data = threadLocal.get(); }).start(); 线程2中设置值 new Thread(() -> { threadLocal.set("线程2的私有数据"); // 为当前线程(线程2)创建副本并赋值 // ... 其他操作 String data = threadLocal.get(); }).start();

3. 必须:使用完毕后调用remove()清理副本(避免内存泄漏)

new Thread(() -> { try { threadLocal.set("临时数据"); // 业务逻辑... } finally { threadLocal.remove(); 无论是否异常,都清理当前线程的副本 } }).start();

实战结果

public class test { private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0); public static void main(String[] args) { Runnable task = () -> { try { int value = threadLocal.get(); value += 1; threadLocal.set(value); System.out.println(Thread.currentThread().getName() + " Value: " + threadLocal.get()); } finally { threadLocal.remove(); } }; Thread thread1 = new Thread(task, "Thread-1"); Thread thread2 = new Thread(task, "Thread-2"); thread1.start(); // 输出: Thread-1 Value: 1 thread2.start(); // 输出: Thread-2 Value: 1 } }

4.ThreadLocal原理

ThreadLocal实际是为每个线程创建一个ThreadLocalMap。ThreadLocalMap通过Entry数组存储数据,Entry数组为key-value结构,key为ThreadLocal对象,value为变量副本(Entry(ThreadLocal 对象, 变量副本))。当线程进行ThreadLocal.get()时,会根据自己的ThreadLocalMap找到对应的变量副本进行读取。


5.ThreadLocal内存泄露问题

内存泄漏同时满足两个条件:

  1. ThreadLocal 实例不再被强引用
  2. 线程持续存活,导致 ThreadLocalMap 长期存在

强引用

  • 最普通的引用方式,即通过赋值操作创建的引用(如Object obj = new Object()中,obj就是强引用)
  • 只要对象被强引用指向,垃圾回收器就不会回收该对象,即使内存不足,JVM 也会抛出OutOfMemoryError而不会回收强引用对象。

弱引用:

  • 一种强度较弱的引用,无法阻止垃圾回收。当对象仅被弱引用指向时,一旦触发垃圾回收,该对象就会被回收(无论内存是否充足)。
  • 弱引用需要通过专门的类(如 Java 的Weak Reference)来创建,不能直接通过赋值生成。

内存泄露的原因

ThreadLocal发生内存泄露的原因是因为内部实现机制导致的。

ThreadLocalMap的key是ThreadLocal对象,ThreadLocal被Weak Reference包装为弱引用,value为强引用。ThreadLocal对象为弱引用,意味着一旦触发垃圾回收,该对象就会被回收,导致ThreadLocalMap的key为null,但value的值还存在ThreadLocalMap中。如果线程持续存活(如:线程池中的核心线程),ThreadLocalMap会长期占用内存,导致内存泄露。如果泄露的内存持续累积,最终会导致内存溢出(OOM,OutOfMemoryError)。

创建强引用指向 ThreadLocal 实例 ThreadLocal<String> tl = new ThreadLocal<>(); 移除强引用(tl 不再指向该实例) tl = null;

通常解决方式是使用后主动调用ThreadLocal.remove()清除value。


6.应用场景

存储登录用户信息

场景:当用户发起请求时,拦截器解析 Token 获取登录用户信息后存入ThreadLocal, Controller、Service、DAO层都可以从ThreadLocal中要拿到当前用户的信息,不需要每层方法都传额外参数。

不使用 ThreadLocal,必须每层传用户 ID

Controller 层 // 从token解析出当前用户id=1001 Long loginUserId = getUserIdByToken(request); // 必须把id当做参数传给service orderService.createOrder(loginUserId, goodsId); Service 层 public void createOrder(Long userId, Long goodsId) { Order order = new Order(); order.setCreateUserId(userId); // 接收上层传过来的id orderMapper.insert(order, userId); // 还要继续传给mapper } Mapper 层 void insert(Order order, Long userId);

使用 ThreadLocal,不用传递任何参数

写一个全局工具类存放用户信息: public class UserContext { private static ThreadLocal<Long> userIdTL = new ThreadLocal<>(); // 存入 public static void setUserId(Long id) { userIdTL.set(id); } // 取出 public static Long getUserId() { return userIdTL.get(); } // 清理 public static void clear() { userIdTL.remove(); } }
Controller层完全不用传 id 给 service orderService.createOrder(goodsId); // 只传业务参数,不用传用户id Service层直接自己获取,不需要上层传入 public void createOrder(Long goodsId) { Long userId = UserContext.getUserId(); // 自己拿,不靠传参 Order order = new Order(); order.setCreateUserId(userId); orderMapper.insert(order); // mapper也不用传userId }

线程池

1.什么是线程池

线程池是一种池化技术,预先创建好一组线程,当有任务要处理时,直接从线程池中获取线程来处理任务,处理完后不会立即销毁线程,而是等待下一个任务执行。从而避免频繁创建线程和销毁线程带的性能开销。


2.如何创建线程池?

1.ThreadPoolExecutor构造函数直接创建

2.Executors工具类

1、newFixedThreadPool():创建一个固定数量的线程池

2、newCachedThreadPool():创建一个可缓存的线程池,

  • 初始核心线程数为 0,最大线程数为Integer.MAX_VALUE(约 20 亿)。
  • 线程复用:当有新任务需要处理,使用空闲的线程
  • 线程回收:超过60秒后线程未使用,线程自动消除

3、newSingleThreadPool():创建一个单线程的线程池

4、newScheduledThreadPool():创建一个定时的线程池

5、newSingleThreadScheduledExecutor():创建一个单线程的定时任务线程池

创建案例:

public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i =0;i<5;i++){ int testId = i; executorService.execute(new Runnable() { @Override public void run() { System.out.println("线程:" + Thread.currentThread().getName() + " 执行任务" + testId); try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); } executorService.shutdown(); }

注意事项

默认使用的队列类型可能带来OOM风险,因此在实际开发中(尤其是服务端),阿里巴巴开发规范不推荐使用Executors直接创建线程池,而是推荐显式使用 ThreadPoolExecutor。

3.七大核心参数(ThreadPoolExecutor 构造函数)

Executors 三大方法的底层,本质上都是调用ThreadPoolExecutor来创建线程池。

ThreadPoolExecutor executor = new ThreadPoolExecutor( int corePoolSize, 核心线程数 int maximumPoolSize, 最大线程数 long keepAliveTime, 空闲存活时间 TimeUnit unit, 时间单位 BlockingQueue<Runnable> workQueue, 阻塞队列 ThreadFactory threadFactory, 线程工厂 RejectedExecutionHandler handler 拒绝策略 );

核心线程数:线程池长期保持存活的线程数量

最大线程数:线程池最多能创建多少个线程。任务队列已满,且当前创建的线程数量已经达到最大线程数,触发拒绝策略。

空闲存活时间:非核心线程空闲后不会立即回收,而是空闲的线程达到存活时间后就会被回收

时间单位:给存活时间加单位,这个时间是 “秒”“毫秒” 还是 “分钟”

阻塞队列:暂时存放待执行的任务。

线程工厂:专门创建线程的地方,而不是直接调用new Thread()

拒绝策略:当任务队列已满且线程数达到最大线程数时,如何处理新任务的方式

4.四种拒绝策略

策略类名策略名称行为说明
AbortPolicy(默认)中止策略❌ 抛出RejectedExecutionException异常,拒绝处理新任务
CallerRunsPolicy调用者运行策略✅ 不让线程池处理新任务,由提交任务的线程自己执行该任务
DiscardPolicy丢弃策略🚫 不处理新任务,直接丢弃任务,不会报错、不会阻塞
DiscardOldestPolicy丢弃最老策略🔁 丢弃最早的未处理的任务
自定义拒绝策略通过实现RejectedExecutionHandler接口实现自定义拒绝策略

5.线程池如何合理设置线程数?

线程池中的线程数如何合理的设置,具体要看是执行什么任务类型,任务类型分为:CPU 密集任务、IO 密集型任务。

  • “4 核 CPU” 是指一个物理 CPU 芯片里,包含 4 个独立的核心,每个核心都能单独执行任务。

1.CPU 密集型任务:任务的执行主要依赖 CPU 的计算能力,大部分时间都在进行逻辑运算、数据处理、循环操作等。线程大部分时间都在占用 CPU 进行计算,特性决定了其线程数不能过多

  • 建议核心线程数=CPU 核心数 + 1

  • 建议最大线程数=核心线程数

  • 例如:图片处理、大量数学计算、复杂算法
  • 场景一:后台数据处理服务,特点稳定流量、任务处理时间长(秒级)、允许一定延迟,线程池的配置可设置如下
    new ThreadPoolExecutor( 8, // corePoolSize = 8(8核CPU) 8, // maximumPoolSize = 8(禁止扩容, 避免资源耗尽) 0, TimeUnit.SECONDS, // 不回收线程 new ArrayBlockingQueue<>(1000), // 有界队列, 容量1000 new CallerRunsPolicy() // 队列满后由调用线程执行 );

2.IO 密集型任务:任务的执行过程中,大部分时间都在等待外部 IO 操作完成(如等待磁盘读写、网络响应、数据库返回结果等)。线程的大部分时间都处于等待状态,CPU处于空闲状态,需要更多线程来 “填满” CPU 的空闲时间。

  • 建议核心线程数=CPU核心数 × 2 或更多

  • 建议最大线程数=核心线程数 x 1.5

  • 例如:数据库查询、文件读写
  • 场景二:电商场景,特点瞬时高并发、任务处理时间短,线程池的配置可设置如下
    new ThreadPoolExecutor( 16, // corePoolSize = 16(假设8核CPU × 2) 32, // maximumPoolSize = 32(突发流量扩容) 10, TimeUnit.SECONDS, // 非核心线程空闲10秒回收 new SynchronousQueue<>(), // 不缓存任务, 直接扩容线程 new AbortPolicy() // 直接拒绝, 避免系统过载 );

6.线程池工作原理

1、默认情况下核心线程不会预先创建,而是有任务提交时逐步创建核心线程。

2、如果核心线程满时,有新任务提交则放入任务队列中等待执行。

3、如果任务队列也满时,会创建非核心线程来处理新任务。

4、如果任务队列已满 + 线程数达到最大线程数时,有新任务提交则触发拒绝策略。

5、当非核心线程空闲时间超过设定的空闲存活时间时,非核心线程会被回收。

7.线程池中submit和execute的区别

相同点:

  • 都是把任务提交给线程池执行

不同点:

  • execute只能提交Runnable类型的任务;
  • submit能提交 Runnable 和 Callable 类型的任务

AQS

1.AQS是什么?

AQS (AbstractQueuedSynchronizer)翻译过来的意思就是 “抽象队列同步器”。

本质是Java中的一个抽象类,他的作用是构建锁、同步器、线程协作工具类的基础框架,底层依靠CLH 双向阻塞队列管理等待线程,CAS来保证原子操作,volatile来保证变量可见性, 最终实现线程的同步。

可重入锁(ReentrantLock)、可重入读写锁(ReentrantReadWriteLock)、信号量(Semaphore),都是基于AQS实现。


2.AQS核心思想

AQS使用一个被Volatile修饰的int类型的 state 变量来表示共享资源的状态,如:

  • ReentrantLock(可重入锁):state表示锁的重入次数。
  • Semaphore(信号量):state表示剩余的可用许可数。
  • CountDownLatch(倒计时器):state表示剩余未递减的计数。

线程获取资源方式,是通过 CAS 尝试修改 state变量的值,修改失败的线程会被封装成一个Node 节点放入到CLH双向阻塞队列中进行排队,只有当持有资源的线程释放资源时,才会唤醒队列中的线程。 被唤醒的线程会重新尝试通过 CAS 获取资源,修改成功:获取资源,执行业务;修改失败:重新进入队列继续阻塞等待。


3.AQS如何使用?

AQS 本身是抽象类,不能直接使用。 使用方式就是:写一个类继承 AQS,重写 tryAcquire 和 tryRelease 方法, 利用 AQS 提供的 state、CAS、队列、park/unpark 能力

// 自己写一把锁 class MyLock { // 内部类 继承 AQS private class Sync extends AbstractQueuedSynchronizer { // 重写:尝试获取锁 @Override protected boolean tryAcquire(int arg) { // CAS 把 state 从 0 → 1 if (compareAndSetState(0, 1)) { // 抢到了! setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // 重写:尝试释放锁 @Override protected boolean tryRelease(int arg) { setExclusiveOwnerThread(null); // state 设回 0 setState(0); return true; } } private Sync sync = new Sync(); // 对外提供 lock public void lock() { sync.acquire(1); // AQS 自带方法 } // 对外提供 unlock public void unlock() { sync.release(1); // AQS 自带方法 } }
MyLock lock = new MyLock(); lock.lock(); // 抢锁 try { // 业务代码 } finally { lock.unlock(); // 释放锁 }

4.三大同步工具(AQS 同步工具类)

【Java并发工具三剑客】CountDownLatch、CyclicBarrier和Semaphore详解 - 佛祖让我来巡山 - 博客园

1.CountDownLatch(闭锁)

内部维护计数器,计数器的初始值代表需要等待的次数,子线程完成任务后调用countDown()减少计数,通过await () 来阻塞主线程执行,当计数归零主线程才开始执行。

2.CyclicBarrier(循环屏障)

一组线程互相等待,当所有线程到达屏障点后,再一起继续执行;执行后自动重置屏障重复使用。

3.Semaphore(信号量)

通过维护固定数量的许可证,用来限制同一时间访问共享资源的线程数量,线程执行前调用acquire()获取许可证,如果没有许可证则阻塞等待;线程使用完资源后调用release()归还许可证


4.三者的区别

核心目的不同

  • CountDownLatch:等待事件完成再持续后续逻辑
  • CyclicBarrier:线程组在屏障点相互等待,全部到位后再持续后续逻辑
  • Semaphore:限制并发访问资源

复用性不同

  • CountDownLatch:计数器归零后失效,不能重复使用
  • CyclicBarrier:一轮结束自动重置计数器,可重复多次使用
  • Semaphore:许可释放后将其归还,可重复多次使用

适用场景不同

  • CountDownLatch 是任务协调器,解决"主线程等子线程"的同步问题
  • CyclicBarrier 是线程同步器,解决"多个线程协同"问题
  • Semaphore 是资源控制器,解决"并发访问量限制"问题

5.三者的应用场景

CountDownLatch

  • 本地测试接口:让一组线程在指定时刻统一触发执行业务,模拟高并发场景。

  • 数据汇总:比如数据详情页需要同时调用多个接口获取数据,并发请求获取到数据后,将数据进行汇总

CyclicBarrier

  • 多阶段数据处理:多线程执行多轮任务,每一轮都要等待所有线程就绪再开始下一阶段

Semaphore

  • 单机限流:可以控制同一时间访问接口、数据库、第三方服务的并发请求数量

6.三者的坑点、异常、业务问题(重点)

CountDownLatch

1.子线程抛异常没执行 countDown () 会发生什么?怎么解决?

现象:假设总任务数 3,1 个子线程没执行 countDown () ,计数器只会降到 2,主线程会一直阻塞。

解决方案:

  1. 使用try-catch 语句块中的 finally 执行 countDown (),无论正常走完还是抛异常,finally一定会执行
  2. 使用带超时 await(),超时后主线程自动放行,避免永久阻塞

2.计数器归 0 后再调用 await () 线程会阻塞吗?

await ()的底层逻辑是:先判断state是否等于 0 ,如果是,直接放行。


CyclicBarrier

1.线程数量少于 等待线程总数 会怎样?

现象:CyclicBarrier barrier = new CyclicBarrier (3); // 等待线程总数 = 3,必须 3 个线程调用 await 才放行,但只开 2 个线程执行 await(),会导致所有线程全部阻塞;

解决方案:使用带超时 await(),超时后抛出异常来结束阻塞


Semaphore

1.release () 为什么要写在 finally?不写会有什么后果(许可泄露)

现象:线程执行业务代码时一旦抛出异常,代码会直接跳出,release 没机会执行,许可证无法归还,当所有许可证弄丢时,后续调用 acquire ()获取许可的线程会全部阻塞卡死。

2.同一线程多次 release () 会怎么样?

现象:Semaphore 不会记录「哪个线程持有许可」,只单纯维护总可用许可数量; 同一线程多次 release () ,会凭空增加可用许可,破坏限流逻辑。​​​​​​​

并发容器

BlockingQueue

JDK 内置 4 种常用阻塞队列

1. ArrayBlockingQueue 有界阻塞队列(生产环境一般都用)

  • 必须指定固定容量,有边界;
  • 队列满后才会创建非核心线程;

2. LinkedBlockingQueue 无界阻塞队列

  • 不指定指定固定容量,任务无限放入队列中
  • 永远不会走到创建非核心线程,最大线程参数失效;

3. SynchronousQueue 不存储元素队列

  • 无容量,插入任务必须等待有线程立刻消费;
  • 提交任务直接尝试新建线程,容易快速达到最大线程;

4. PriorityBlockingQueue 优先级无界队列

  • 按任务优先级排序执行,无界;
  • 不按提交顺序,适合需要优先级调度的任务。

ConcurrentHashMap

他是HashMap的线程安全版本,与HashMap不同的是,他不允许为null键或值,底层在JDK1.7时靠Segment 分段锁保证线程安全,在JDK1.8后靠CAS + synchronized 保证线程安全。

  • 分段锁:通过将数据分成多个段(Segment),每个分段拥有 ReentrantLock 锁;

ConcurrentHashMap​​​​​​​的数据结构

JDK1.7:Segment数组+HashEntry数组+链表的结构

ConcurrentHashMap内部使用了一个名为segments 的数组结构,数组中每个元素是 Segment 对象,该内部类包含一个HashEntry数组用来存储数据,并且继承自ReentrantLock

//Segment数组 final Segment<K,V>[] segments;
static final class Segment<K,V> extends ReentrantLock implements Serializable { transient volatile HashEntry<K,V>[] table; }

JDK1.8:数组+链表+红黑树的结构

  • 默认数组大小:16,负载因子:0.75
  • 链表长度 ≥ 8时转换为红黑树(树化)
  • 树节点数 ≤ 6时退化为链表(反树化)

应用场景

只要是多线程并发读写的场景,无论数据量大小,都优先使用 ConcurrentHashMap; 如果是单线程,使用 HashMap。例如:

  • 网站访问量统计:大量用户同时访问网站,多条线程实时累加每个页面的点击次数。
  • 商品库存实时更新:大量用户同时读取商品库存,后台线程实时更新剩余库存。

CompletableFuture

Java8 新增的异步多线程工具,用来简化异步任务、线程等待、任务编排的。​​​​​​​

相关新闻

  • 题解:学而思编程 子矩阵的和
  • Apollo自动驾驶系统深度解密:从传感器到控制器的完整技术架构解析
  • DriveStudio深度解析:高效构建城市级3D高斯场景重建与仿真的一站式方案

最新新闻

  • YOLO目标检测中的异常输入处理与优化策略
  • Maze勒索病毒与Spelevo漏洞利用包的组合攻击链深度解析与防御实践
  • PIC18F26K42与MC74HC165A实现多路输入扩展方案
  • 水下图像增强技术:波长补偿与去雾算法详解
  • 解放你的Alienware:500KB轻量工具替代臃肿AWCC的终极指南
  • YOLO11网络结构深度解析与实现细节

日新闻

  • 基于YOLOv12的番茄成熟度智能检测系统开发
  • 终极RimWorld模组管理指南:用RimSort告别模组冲突烦恼
  • AI Agent框架开发:从理论到实践的完整指南

周新闻

  • 基于YOLOv12的番茄成熟度智能检测系统开发
  • 终极RimWorld模组管理指南:用RimSort告别模组冲突烦恼
  • AI Agent框架开发:从理论到实践的完整指南

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号