)
ThreadLocal1.ThreadLocal是什么ThreadLocal是Java用于实现“线程本地存储变量”的工具类。他的作用是为每个线程创建独有的变量副本线程只能读写自己的副本所以不存在多线程竞争资源问题更不需要加锁来实现线程安全。2.为什么要使用ThreadLocal同一份数据被多线程同时访问会出现线程安全问题通常解决方案是加锁但加锁会造成阻塞、会降低并发性能。而ThreadLocal 天然线程安全不需要加锁也能够提升并发性能。3.ThreadLocal如何使用标准使用步骤3 步1. 定义ThreadLocal实例全局唯一通常用static修饰方式1初始值为 null private static ThreadLocalString threadLocal new ThreadLocal(); 方式2用 withInitial 指定初始值JDK8 private static ThreadLocalInteger 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 ThreadLocalInteger 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内存泄露问题内存泄漏同时满足两个条件ThreadLocal 实例不再被强引用线程持续存活导致 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会长期占用内存导致内存泄露。如果泄露的内存持续累积最终会导致内存溢出OOMOutOfMemoryError。创建强引用指向 ThreadLocal 实例 ThreadLocalString tl new ThreadLocal(); 移除强引用tl 不再指向该实例 tl null;通常解决方式是使用后主动调用ThreadLocal.remove()清除value。6.应用场景存储登录用户信息场景当用户发起请求时拦截器解析 Token 获取登录用户信息后存入ThreadLocal Controller、Service、DAO层都可以从ThreadLocal中要拿到当前用户的信息不需要每层方法都传额外参数。不使用 ThreadLocal必须每层传用户 IDController 层 // 从token解析出当前用户id1001 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 ThreadLocalLong 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;i5;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, 时间单位 BlockingQueueRunnable 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 88核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 类型的任务AQS1.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.三者的坑点、异常、业务问题重点CountDownLatch1.子线程抛异常没执行 countDown () 会发生什么怎么解决现象假设总任务数 31 个子线程没执行 countDown () 计数器只会降到 2主线程会一直阻塞。解决方案使用try-catch 语句块中的 finally 执行 countDown ()无论正常走完还是抛异常finally一定会执行使用带超时 await()超时后主线程自动放行避免永久阻塞2.计数器归 0 后再调用 await () 线程会阻塞吗await ()的底层逻辑是先判断state是否等于 0 如果是直接放行。CyclicBarrier1.线程数量少于 等待线程总数 会怎样现象CyclicBarrier barrier new CyclicBarrier (3); // 等待线程总数 3必须 3 个线程调用 await 才放行但只开 2 个线程执行 await会导致所有线程全部阻塞解决方案使用带超时 await()超时后抛出异常来结束阻塞Semaphore1.release () 为什么要写在 finally不写会有什么后果许可泄露现象线程执行业务代码时一旦抛出异常代码会直接跳出release 没机会执行许可证无法归还当所有许可证弄丢时后续调用 acquire ()获取许可的线程会全部阻塞卡死。2.同一线程多次 release () 会怎么样现象Semaphore 不会记录「哪个线程持有许可」只单纯维护总可用许可数量 同一线程多次 release () 会凭空增加可用许可破坏限流逻辑。并发容器BlockingQueueJDK 内置 4 种常用阻塞队列1. ArrayBlockingQueue 有界阻塞队列生产环境一般都用必须指定固定容量有边界队列满后才会创建非核心线程2. LinkedBlockingQueue 无界阻塞队列不指定指定固定容量任务无限放入队列中永远不会走到创建非核心线程最大线程参数失效3. SynchronousQueue 不存储元素队列无容量插入任务必须等待有线程立刻消费提交任务直接尝试新建线程容易快速达到最大线程4. PriorityBlockingQueue 优先级无界队列按任务优先级排序执行无界不按提交顺序适合需要优先级调度的任务。ConcurrentHashMap他是HashMap的线程安全版本与HashMap不同的是他不允许为null键或值底层在JDK1.7时靠Segment 分段锁保证线程安全在JDK1.8后靠CAS synchronized 保证线程安全。分段锁通过将数据分成多个段Segment每个分段拥有 ReentrantLock 锁ConcurrentHashMap的数据结构JDK1.7Segment数组HashEntry数组链表的结构ConcurrentHashMap内部使用了一个名为segments 的数组结构数组中每个元素是 Segment 对象该内部类包含一个HashEntry数组用来存储数据并且继承自ReentrantLock//Segment数组 final SegmentK,V[] segments;static final class SegmentK,V extends ReentrantLock implements Serializable { transient volatile HashEntryK,V[] table; }JDK1.8数组链表红黑树的结构默认数组大小16负载因子0.75链表长度 ≥ 8时转换为红黑树(树化)树节点数 ≤ 6时退化为链表(反树化)应用场景只要是多线程并发读写的场景无论数据量大小都优先使用 ConcurrentHashMap 如果是单线程使用 HashMap。例如网站访问量统计大量用户同时访问网站多条线程实时累加每个页面的点击次数。商品库存实时更新大量用户同时读取商品库存后台线程实时更新剩余库存。CompletableFutureJava8 新增的异步多线程工具用来简化异步任务、线程等待、任务编排的。