
直接从正文开始。写《Java多线程从基础到高级应用》这篇内容之前我认真想了想。标题里的基础和高级四个字其实点出了绝大多数Java开发者的真实状态会写new Thread()会加synchronized但一到线程池参数调优、锁的底层原理、并发容器选型就发怵。面试一问volatile能不能保证原子性就露怯。这篇分享我想按一条完整的学习主线来梳理尽量把每个知识点背后的为什么讲透——为什么会有内存可见性问题为什么Lock在特定场景下优于synchronized为什么线程池不能随便new一个。内容覆盖面比较广从线程的创建与生命周期到JUC工具类、并发容器、线程池再到锁优化与实战排查把多线程这条线的核心骨架都串起来。不管你是刚入门还是准备面试顺着这条线走基本能把多线程的体系搭建清楚。1. 多线程到底在学什么先把整体脉络理清楚1.1 很多人学多线程容易卡住原因不在代码我见过太多人学多线程时陷入一个怪圈API都认识Thread、Runnable、ExecutorService都能背出来但一写多线程程序就出问题——数据错乱、死锁、性能反而变差。问题在于多线程不是一个语法知识点而是一套并发编程思维模型。synchronized只是工具真正难的是判断这段共享数据是否需要保护用什么粒度的锁会不会发生死锁线程之间如何协作。所以我在梳理学习路线时第一件事不是列API而是帮助建立三层认知框架硬件层多核CPU、缓存一致性问题这是volatile和内存屏障存在的根源JVM层JMMJava内存模型定义了线程与主内存之间的交互规则synchronized、volatile、final都是围绕JMM展开的应用层锁、线程池、并发容器、并发工具类这些是写业务代码时真正会接触的东西大部分教程直接从第三层讲起所以读者会觉得都是零散API。我的建议是哪怕硬着头皮也要先把JMM的内存可见性、原子性、有序性三个概念吃透。因为你会发现后续所有锁机制、并发容器、线程池本质上都在解决这三个问题。1.2 学习路线设计三个阶段递进我给内容划了三个阶段你可以对照自己的位置入门阶段能创建线程、理解生命周期、会处理简单的线程安全问题进阶阶段掌握Lock体系、JUC并发工具、并发容器、线程池的原理与用法高级阶段理解CAS、AQS、锁优化、死锁排查、性能调优能输出合理的并发设计方案第一阶段解决会用第二阶段解决好用的工具链第三阶段解决为什么要这么设计以及线上出问题怎么排查。这正好对应这篇分享的章节安排。1.3 多线程不是银弹先确认你确实需要并发聊几个值得记住的场景。多线程最常见的收益是提高吞吐量——比如一个任务IO等待5秒单线程只能串行处理多线程可以让等待时间重叠。其次是更合理的CPU资源利用——8核机器只跑单线程等于7核在闲着。但多线程也引入三个额外成本上下文切换开销、锁竞争开销、并发Bug排障成本。我见过不少团队一个简单的计数场景非要上多线程加锁结果吞吐量反而比单线程还低。所以设计之前先回答一个问题这个任务是CPU密集、IO密集还是几乎不耗时CPU密集型线程数建议CPU核数 1加锁要谨慎IO密集型线程数可以放到CPU核数 * 2甚至更多等待期间让出CPU任务粒度极小的单线程反而更快因为线程创建和切换的代价远超收益理顺这套思路之后下面所有技术点就有了落点。2. 线程基座创建方式、生命周期与基础API2.1 三种创建方式以及各自适合的场景老生常谈的三种方式——继承Thread、实现Runnable、实现Callable配合FutureTask。但关键不只是写法而是你要知道它们背后的语义差异// 方式一继承Thread适合快速原型但不推荐业务使用 class MyThread extends Thread { Override public void run() { System.out.println(继承Thread方式运行: Thread.currentThread().getName()); } } // 方式二实现Runnable推荐因为Java单继承实现接口不影响继承其他类 Runnable task () - System.out.println(Runnable方式运行: Thread.currentThread().getName()); // 方式三Callable带返回值适合需要计算结果、抛异常的异步任务 CallableInteger callable () - { Thread.sleep(1000); return 1 1; }; FutureTaskInteger futureTask new FutureTask(callable); new Thread(futureTask, 带返回值线程).start(); Integer result futureTask.get(); // get()会阻塞直到拿到结果 System.out.println(计算结果: result);FutureTask.get()是一个很容易踩坑的点——拿到结果的必经之路但如果你不确认任务是否完成就直接get()当前线程会一直阻塞等待。这个阻塞可以被中断但如果你在主线程里调get()而不设置超时时间任务又卡住了主线程也会被拖死。建议用带超时的get(long timeout, TimeUnit unit)实话说我线上排查过的不少接口响应缓慢案例源头就是get()没有超时。2.2 线程生命周期别只背六个状态NEW新建、RUNNABLE可运行/运行中、BLOCKED阻塞、WAITING等待、TIMED_WAITING计时等待、TERMINATED终止这六个状态很多面试题都考但更实用的是搞清楚状态之间怎么流转。我画了很久的流程图发现关键动作就几个start()从NEW进入RUNNABLE获得CPU时间片RUNNABLE里运行失去时间片或IO等待回到RUNNABLE排队进入synchronized块但没拿到锁RUNNABLE - BLOCKED调用wait()/join()/park()进入WAITING调用sleep(ms)/wait(timeout)进入TIMED_WAITINGrun()跑完或抛异常TERMINATED有意思的是很多人误以为WAITING和BLOCKED是一回事。其实BLOCKED是被动抢锁失败WAITING是主动让自己停下来等通知。sleep()不会释放锁wait()会释放锁——这是面试高频考点也是写多线程最容易弄混的地方。2.3 常用API细节可能跟你理解的不一样几个高频API的细节说一下。start()和run()的区别这个我能理解为数不多还在考的基础题——run()只是普通方法调用在调用者线程里执行start()才是让JVM创建一个新线程并调度执行。join()的含义是当前线程等这个线程执行完再继续我实际使用中更喜欢用CountDownLatch做多个线程的汇合等待语义更清晰。interrupt()也是一个容易误用的API。它并不会强制打断线程的执行而是设置一个中断标志位。被中断的线程如果正在sleep()、wait()、join()这些可中断方法里会直接抛出InterruptedException并清掉中断标志。如果线程在跑普通的业务循环你需要自己检查Thread.currentThread().isInterrupted()来响应中断。3. 线程安全的第一道防线synchronized 与 volatile3.1 并发Bug的三个根源可见性、原子性、有序性我之前说过这是整片内容的地基。可见性问题来源于CPU缓存——每个线程都可能有自己的工作缓存两个线程各自读到一个共享变量的不同副本互相看不到对方的修改。原子性问题来源于线程的读-改-写操作被交替执行——比如count它不是一条指令而是读、加一、写回三步两步之间线程可能被切换。有序性问题来源于编译器和CPU为了优化而进行的指令重排单线程下重排不影响结果多线程下就会出幺蛾子。volatile解决的是可见性和有序性不解决原子性。这一点必须反复强调volatile修饰的int count做count还是线程不安全的。synchronized则同时解决三个问题——互斥执行保证原子性锁的释放和获取建立先前发生关系保证可见性临界区内的代码不会被重排到锁边界之外。3.2 synchronized 用法与锁升级机制三种用法大家应该熟修饰实例方法锁的是this修饰静态方法锁的是Class对象修饰代码块锁的是括号里的对象。我的经验是能用代码块就别锁整个方法锁的粒度越小并发度越高。// 锁的是当前实例 public synchronized void instanceMethod() { ... } // 锁的是Class对象所有实例共享 public static synchronized void staticMethod() { ... } // 锁指定对象粒度最细 public void blockMethod() { synchronized (lockObject) { // 临界区 } }更值得关注的是JDK 6之后的锁升级机制。早期synchronized是重量级锁性能差所以才有了ReentrantLock的补位。但现代JVM做了大量优化锁状态从无锁到偏向锁、轻量级锁、重量级锁逐级升级。偏向锁假设同一个线程反复获取同一把锁用一个CAS记录线程ID就完事如果发生竞争升级为轻量级锁自旋等待自旋超过阈值或者竞争激烈才膨胀为重量级锁交给操作系统内核阻塞。这个机制非常像小区门禁——人少的时候不设岗人多的时候才安排保安。所以现在很多场景下synchronized和ReentrantLock的性能差距已经非常小了可读性还更好。3.3 volatile 适用场景以及它的边界我建议把volatile用在三种场景状态标志位如volatile boolean running true、单例模式的双重检查锁DCL中的instance字段、以及一写多读的配置项。经典DCL代码public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance null) { // 第一次检查 synchronized (Singleton.class) { if (instance null) { // 第二次检查 instance new Singleton(); } } } return instance; } }这里volatile的关键作用是禁止指令重排。new Singleton()在底层有三步分配内存、初始化对象、把引用赋值给变量。如果不加volatile第三步可能被重排到第二步之前另一个线程在第一次检查时看到非空引用拿到了一个尚未初始化完成的对象——这是真实发生过的经典并发Bug。但如果你试图用volatile做原子计数器、做复杂复合操作那就会踩坑。、都不是原子的volatile帮不了你。4. 显式锁体系Lock、Condition 与 AQS 的核心机制4.1 ReentrantLock vs synchronized怎么选synchronized是隐式锁加锁解锁由JVM控制ReentrantLock是显式锁你需要手动lock()和unlock()通常配合try/finally使用确保解锁一定会执行。ReentrantLock lock new ReentrantLock(); try { lock.lock(); // 临界区 } finally { lock.unlock(); }ReentrantLock独有的能力值得记住可中断lockInterruptibly()、可超时tryLock(timeout)、公平锁构造参数传true、支持多个Condition队列。可超时这一点实用性极高——拿不到锁就放弃而不是无限等下去这对接口超时控制很有用。注意tryLock()返回false时不代表业务失败它只是告诉你锁没抢到。你要根据场景决定是重试还是放弃。4.2 读写锁与公平锁的取舍ReentrantReadWriteLock把锁拆分成读锁和写锁读读不互斥读写、写写互斥。这非常适合读多写少的场景典型如缓存系统。但有个坑——写锁饥饿。如果读线程源源不断写线程可能一直等不到锁。处理办法是使用JDK 8引入的StampedLock它的乐观读能做到读写完全不阻塞不过使用门槛高容易出现不正确的用法比如乐观读后忘记重新检查版本号我一般只在高性能场景才推荐。公平锁解决的是线程饥饿问题但它有代价额外维护队列顺序、上下文切换更频繁吞吐量通常会低于非公平锁。我个人的原则是默认非公平锁只有在明确要求先来先服务时才用公平锁。因为大多数业务场景短任务的延迟抖动远没有饥饿问题严重。4.3 Condition比 wait/notify 更灵活的线程协作wait()/notify()的问题在于它没法精确唤醒某一类线程。比如一个队列有生产者和消费者两类线程你notifyAll()会全部唤醒然后大部分线程白白抢锁又阻塞。Condition则可以把线程注册到不同的等待队列里ReentrantLock lock new ReentrantLock(); Condition notEmpty lock.newCondition(); Condition notFull lock.newCondition(); // 生产者 lock.lock(); try { while (queue.size() MAX) { notFull.await(); // 队列满等待不满信号 } queue.offer(item); notEmpty.signal(); // 通知消费者不空 } finally { lock.unlock(); } // 消费者 lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); // 队列空等待不空信号 } queue.poll(); notFull.signal(); // 通知生产者不满 } finally { lock.unlock(); }这里有个细节一定要用while包住await()不能用if。因为线程被唤醒后还要重新检查条件是否成立存在虚假唤醒spurious wakeup的可能而且多个消费者同时被唤醒时一个消费者取走数据另一个醒来时队列可能又空了。多线程面试一年到头最常考的生产者消费者模型就是上面这段。5. JUC 并发工具与并发容器生产环境的常客5.1 CountDownLatch、CyclicBarrier、Semaphore 三个工具对比这三个工具搞混的人特别多我用一张表整理清楚工具核心语义典型场景关键注意点CountDownLatch计数器倒数归零后放行主线程等N个子任务完成计数不能重置是一次性的CyclicBarrier线程互相等待都到齐后一起放行分批计算、分阶段任务可循环使用支持重置Semaphore许可证数量控制同时并发数限流、连接池控制需要手动释放小心泄漏CountDownLatch和CyclicBarrier的区别是理解重点。CountDownLatch是倒数门闩一个线程等多个线程等的是计数归零CyclicBarrier是循环栅栏多个线程等彼此等的是人到齐。CountDownLatch是一次性的CyclicBarrier可以循环使用。Semaphore的限制流量非常实用。比如某个下游数据库只能承受5个并发查询你用一个Semaphore(5)来控制超过5个就等待而不是一次性打爆下游。5.2 并发容器为什么不用 HashMap 和 ArrayListHashMap在多线程下的问题网上有无数案例——JDK 7里扩容时可能形成循环链表造成CPU 100%的囧境虽然JDK 8改成了尾插法但并发下数据丢失仍是常态。ArrayList的并发问题也一样。生产环境直接用这些替代品ConcurrentHashMap分段锁JDK 7或CASsynchronized锁桶JDK 8实现读多写少下性能极好CopyOnWriteArrayList写时复制适用于读多写极少、数据量小的场景如监听器列表BlockingQueue系列ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue天然支持线程安全的生产者消费者ConcurrentLinkedQueue无界非阻塞队列适合高并发下不需要阻塞的场景一个选型经验写多读少用ConcurrentHashMap读极多写极少用CopyOnWriteArrayList有界缓冲用ArrayBlockingQueue无界缓冲用ConcurrentLinkedQueue。别一上来就什么都用ConcurrentHashMap它的迭代弱一致性和size()统计不精确在某些场景并不合适。5.3 ThreadLocal线程隔离的利器以及它的内存泄漏坑ThreadLocal的本质是每个线程持有自己的一份变量副本它是空间换时间、空间换隔离的思路。典型场景SimpleDateFormat线程不安全每个线程放一个自己的实例又比如分布式链路追踪里传递traceId。private static final ThreadLocalSimpleDateFormat DATE_FORMAT ThreadLocal.withInitial(() - new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); String format(Date date) { return DATE_FORMAT.get().format(date); }最大的坑是内存泄漏。ThreadLocalMap的Entry继承WeakReferenceKey是弱引用但Value是强引用。如果线程长期存活典型如线程池线程而ThreadLocal的外部强引用被清掉了Key被GCValue却依然被Entry强引用着就永远无法回收。使用后用remove()清理比什么都重要。尤其在线程池中复用线程的场景你不清理下一个任务还可能读到上一个任务的脏数据——这比内存泄漏更隐蔽、更具破坏性。6. 线程池生产环境最考功力的地方6.1 ThreadPoolExecutor 七大参数逐一说透线程池设计核心就是ThreadPoolExecutor那七个参数。面试必考实话说能把这七个参数说清楚并给出合理值的基本都是真正做过生产系统的参数含义经验值参考corePoolSize核心线程数即使空闲也保留CPU密集型N1IO密集型2NmaximumPoolSize最大线程数含核心与临时线程视任务阻塞情况一般不能超过某个上限keepAliveTime临时线程空闲存活时间常见30秒~60秒workQueue任务队列阻塞队列不要用无界队列会有DB连接等资源打满风险threadFactory线程工厂设置线程名务必自定义线上排查全靠线程名handler拒绝策略默认AbortPolicy直接抛异常生产环境看场景改unitkeepAliveTime的时间单位秒/毫秒生产环境我最强调两点。第一一定要自定义threadFactory给线程起个有意义的名字否则线上jstack出来一堆pool-1-thread-1查问题连哪个线程跑哪个任务都分不清。第二队列要有界并且要配合合理的拒绝策略。无界队列看似安全但任务无限堆积占满内存慢的是整个应用。6.2 任务执行流程以及拒绝策略怎么选线程池的执行流程很多人背不下来我帮你翻译成大白话当前线程数 corePoolSize直接创建核心线程执行任务当前线程数 corePoolSize任务塞进workQueue排队队列满了 当前线程数 maximumPoolSize创建临时线程执行任务队列满了 线程数已达maximumPoolSize走拒绝策略这里有个反直觉的地方线程池是先填队列再扩线程不是上来就疯狂建线程。核心线程满了新任务先在队列里排队等到排不下才扩大线程数。所以任务量是突刺型还是持续型直接影响你设置队列长度的策略。拒绝策略有四种AbortPolicy默认直接抛RejectedExecutionExceptionCallerRunsPolicy谁提交谁去跑把任务退回调用线程执行DiscardPolicy静默丢弃DiscardOldestPolicy丢弃队列里最老的任务我实际用CallerRunsPolicy比较多它能在系统过载时自然降速——调用方既要提交任务又要自己执行任务提交速度就慢了相当于一个反馈回路。6.3 线程池生产环境的几个坑**坑一局部变量new线程池。**高并发下每个请求进来都new一个ThreadPoolExecutor线程池创建销毁的成本远超你的想象还容易撑爆内存。线程池应该做成全局的、按业务隔离的组件。坑二线程池里的异常被吞掉。execute()方式提交的任务如果run()里抛异常异常会直接传给线程池的UncaughtExceptionHandler你不设置的话线程会默默销毁然后新建一个——表面上没事任务却已经丢了。建议用submit()方式提交异常会被包装在Future里调用future.get()就能拿到或者在run()里自己try/catch并记录错误。坑三关闭线程池的姿势。shutdown()是等已提交任务执行完再关shutdownNow()是立刻中断所有任务并返回等待队列里尚未执行的任务列表。生产环境建议用shutdown()配合一个超时判断是否真的终止了。7. 锁优化、CAS、ConcurrentHashMap 原理与综合实战7.1 锁优化思路从乐观到悲观、从粗到细高级阶段的核心是锁优化思维。首先能不用锁就不用锁。比如AtomicInteger的CAS操作比synchronized更轻量适合竞争不激烈的计数场景。CAS的核心是比较并交换——你期望内存值等于某个旧值是就更新不是就重试。它没有锁的阻塞唤醒开销但在高竞争下会有大量自旋白白烧CPU。其次如果能缩小临界区就缩小。比如不要锁住整个方法只锁需要保护的字段操作。再有读写分离。读多写少用读写锁、用CopyOnWriteArrayList。最后减少锁竞争。用ThreadLocal把一份数据变成多份副本把共享变成隔离。还有一个常被忽略的优化LongAdder。在高并发计数场景AtomicLong的CAS自旋会成为瓶颈LongAdder在内部维护多个Cell不同线程分散到不同Cell上累加最后求和把竞争摊薄。本质上是分段计数的思想跟ConcurrentHashMap的分桶是同一个套路。7.2 ConcurrentHashMap 为什么又安全又快这个问题值得单独说因为它把锁优化用得淋漓尽致。JDK 8的实现抛弃了分段锁改成了CAS synchronized锁桶。put()的大致逻辑先对key的hash做一次扰动定位到桶如果桶为空CAS直接插入如果桶非空synchronized锁住这个桶的头节点再处理链表或红黑树。这样不同桶之间的插入互不阻塞只有同一个桶的写操作才竞争。get()完全无锁Node的val和next都用volatile修饰保证可见性。所以ConcurrentHashMap在读多写少场景下性能极好。要注意的边角问题它的size()返回的是估算值因为并发mappingCount需要加锁统计不是精确值它的迭代器是弱一致性的迭代过程中看到的数据可能是过时的。7.3 一个综合案例多线程并发查询控制速率并汇总结果把前面的知识串起来我写一个典型的实战场景批量查询一堆商品ID的价格信息调用第三方接口限制并发数为5全部完成后统一汇总且总耗时不能超过10秒。public class PriceQueryTask { private final ExecutorService pool; private final Semaphore semaphore new Semaphore(5); public PriceQueryTask() { // 核心线程数3最大8队列容量8拒绝策略调用者执行 pool new ThreadPoolExecutor( 3, 8, 30L, TimeUnit.SECONDS, new ArrayBlockingQueue(8), r - new Thread(r, price-query- r.hashCode()), new ThreadPoolExecutor.CallerRunsPolicy() ); } public MapString, BigDecimal batchQuery(ListString productIds, long timeoutMs) throws InterruptedException { CountDownLatch latch new CountDownLatch(productIds.size()); MapString, BigDecimal resultMap new ConcurrentHashMap(); for (String productId : productIds) { pool.submit(() - { try { semaphore.acquire(); try { // 模拟调用第三方价格接口 BigDecimal price queryRemotePrice(productId); resultMap.put(productId, price); } finally { semaphore.release(); } } catch (Exception e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }); } boolean completed latch.await(timeoutMs, TimeUnit.MILLISECONDS); pool.shutdown(); return resultMap; } private BigDecimal queryRemotePrice(String productId) { return BigDecimal.valueOf(Math.random() * 100); } }这段代码里Semaphore(5)把第三方接口的并发数限制住了ConcurrentHashMap让多个线程并发写结果集而不冲突CountDownLatch让主线程统一等待汇总latch.await(timeout)加了超时控制避免接口慢拖垮主流程。你会看到前面的工具在这里全部自然组合起来。8. 常见问题排查实录与项目经验总结8.1 排查多线程问题的三板斧线上多线程问题最怕偶现。稳定的解决方案是条理化的排查流程线程转储thread dump第一时间jstack抓现场。看线程状态是WAITING还是BLOCKED等哪把锁被谁持有。死锁的两个线程往往会互相持有对方需要的锁jstack里会直接给你警示提示。内存与垃圾回收看GC日志和堆内存快照。多线程问题往往伴随内存泄漏比如ThreadLocal不清理、频繁FullGC、CPU飙高。代码审查重点查三个地方——共享变量有没有正确的同步保护、锁的获取顺序是否一致不一致极容易死锁、线程池的参数是不是生产环境下瞎拍的数字。排查工具有的话用arthas线上动态查看线程栈、方法调用耗时、线程池状态都很好用比各种付费监控轻量很多是我处理并发问题现场的第一选择。8.2 死锁的现场复盘死锁的四个必要条件互斥、持有并等待、不可剥夺、循环等待。第一次写死锁的时候我自己都觉得不可思议——代码只要能跑逻辑上似乎永远触发不了但高并发下它偏偏就发生了。一个真实的复盘案例两个线程分别持有锁A和锁B非要同时去拿对方的锁。一个典型的规避方式统一加锁顺序比如所有线程都先拿A再拿B破坏循环等待。另一个方式是用tryLock(timeout)拿不到就放弃并释放已有锁避免无限等下去。实话说比起事后排查死锁我更推荐在代码评审阶段就检查加锁顺序。8.3 几个经验之谈也是我踩过的坑**单例模式里的DCL加不加volatile选型要想清楚。**很多人默认不加也能跑但那是靠运气——构造方法里的操作恰好没有危险重排。规范写法就是加面试官挑不出毛病线上也排掉了隐患。**线程池里的线程名称一定要起对。**一个凌晨的线上告警压测时线程池被打满没自定义线程名的线程池里全是pool-1-thread-3想定位它属于哪个业务模块只能靠猜。后来所有线程池都用ThreadFactoryBuilder起名字一眼能看出是哪个服务的哪个模块。**SimpleDateFormat是垃圾别共享。**它的内部Calendar不是线程安全的并发调用format()会偶发抛出NumberFormatException或者结果错乱。要么每次new一个要么用ThreadLocal包起来要么用JDK 8的DateTimeFormatter它是线程安全的。写日期格式化代码三选一别偷懒。**不要迷信越高并发越好。**多线程提升性能是有限的线程多了上下文切换、缓存失效、锁竞争都会放大。我曾经把一个服务的线程池从50调到200结果吞吐量先涨后跌最后稳定在120左右反而比50还差。配参数要边调边观察别拍脑袋。9. 收尾这套内容怎么用以及我的一点体会这份整理覆盖了从线程创建到线程池调优的完整链路也串起了synchronized、Lock、CAS、JUC、并发容器、死锁排查这些必考核心点。你在学习中我建议按顺序过一遍每看到一个锁机制就追问三个问题它解决什么问题代价是什么适合什么场景三种答案都清楚了才算真正吃透。实话说学多线程没有捷径最快的方式就是在真实项目里踩坑。我第一次用ConcurrentHashMap做计数器结果数据还是丢了排查了两个小时才发现是检查-更新这个复合操作不属于原子操作第一次调线程池参数压测时接口超时率飙升到30%才发现队列和拒绝策略没配好。但正是这些坑让我真正明白了为什么要用AtomicLong、为什么要用有界队列、为什么要用CallerRunsPolicy。如果你现在正卡在多线程的某个知识点上我的建议很朴素**动手写一个小案例然后故意制造一个并发Bug再用调试工具如jvisualvm、jstack、arthas把它排查出来。**这个闭环走一遍比盯着文档看十遍都管用。多线程是真的纸上得来终觉浅绝知此事要躬行祝你在并发这条路上早日打通任督二脉。