核心理念:理解问题 -> 学习工具 -> 动手实践 -> 深入原理
详细学习路线 & 阶段指南:
阶段 1:线程基础与生命周期 (打好根基)
- 目标: 理解什么是线程,如何在Java中创建和启动线程,线程的生命周期状态及其转换。
-   核心知识点:
-   Thread类:-   继承 Thread类并重写run()方法。
-   创建 Thread实例,调用start()方法启动线程 (理解start()和run()的区别!)。
-   线程命名 (setName(),getName())。
 
-   继承 
-   Runnable接口:-   实现 Runnable接口并实现run()方法。
-   将 Runnable实例传递给Thread构造函数 (推荐方式,更灵活,避免单继承限制)。
-   理解 Thread和Runnable的关系:Thread本身也是Runnable。
 
-   实现 
-   线程生命周期:
-   NEW: 创建后尚未start()。
-   RUNNABLE: 调用start()后,在JVM中等待CPU时间片或正在运行。(注意:操作系统层面的Running和Ready在JAVA中都映射为RUNNABLE)。
-   BLOCKED: 等待获取一个监视器锁 (如进入synchronized块/方法,但锁被其他线程占用)。
-   WAITING: 无限期等待,直到被其他线程显式唤醒。调用Object.wait(),Thread.join()(无参数),LockSupport.park()会进入此状态。
-   TIMED_WAITING: 有限期等待。调用Thread.sleep(long),Object.wait(long),Thread.join(long),LockSupport.parkNanos(),LockSupport.parkUntil()会进入此状态。
-   TERMINATED: 线程执行完毕 (run()方法结束) 或异常退出。
 
-   
-   常用方法:
-   sleep(long millis): 静态方法,让当前线程休眠指定毫秒数,不释放锁。会进入TIMED_WAITING。
-   yield(): 静态方法,提示调度器当前线程愿意让出CPU,但调度器可以忽略。主要用于调试或测试,实际意义不大。
-   join()/join(long millis): 等待调用此方法的线程终止。比如在main线程中调用thread.join(),则main线程会阻塞,直到thread执行完毕。
-   interrupt(): 中断目标线程。中断只是一个协作机制,需要目标线程检查中断状态并处理。
-   isInterrupted(): 检查线程是否被中断 (不清除中断标志)。
-   static interrupted(): 静态方法,检查并清除当前线程的中断状态。
-   setPriority(int)/getPriority(): 设置/获取线程优先级 (1-10)。强烈建议不要依赖优先级进行程序逻辑控制,因为不同操作系统平台对优先级的映射和支持不同,效果不可预测。
-   setDaemon(boolean)/isDaemon(): 设置/检查是否为守护线程。守护线程不会阻止JVM退出 (当所有非守护线程结束时,JVM退出,守护线程会被强制终止)。
 
-   
 
-   
-   实践练习:
-  分别用继承 Thread和实现Runnable创建多个线程,打印不同信息。
-  观察 start()和直接调用run()的区别。
-  使用 sleep()模拟耗时操作,观察线程状态 (Thread.getState())。
-  练习 join():主线程等待子线程结束再继续。
-  练习 interrupt()和中断处理:在run()方法中循环检查Thread.interrupted()或isInterrupted(),收到中断请求后优雅退出循环。
- 创建守护线程和非守护线程,观察JVM退出的行为差异。
 
-  分别用继承 
-   关键细节 & 坑:
-   start()只能调用一次,多次调用会抛IllegalThreadStateException。
-   sleep()是Thread的静态方法,作用于当前线程。
-   yield()效果不确定,不要用它来做同步或控制流程。
- 线程优先级是提示性的,不要依赖它保证执行顺序。
-   守护线程中执行的代码(如 finally块)在JVM退出时可能没有机会执行,避免在守护线程中执行关键资源释放操作。
- 理解线程状态转换图是基础中的基础!
 
-   
阶段 2:线程安全与同步 (核心!核心!核心!)
-   目标: 理解什么是线程安全问题 (竞态条件),掌握保证线程安全的机制:锁 (synchronized)、可见性 (volatile)、显式锁 (Lock)、原子类 (AtomicXXX)。
- 
核心知识点: -   线程安全问题根源:
- 竞态条件 (Race Condition): 多个线程以不一致的顺序访问和操作共享数据,导致结果依赖于线程执行的时序。
- 内存可见性问题 (Memory Visibility): 一个线程对共享变量的修改,另一个线程不一定能立即看到 (由于CPU缓存、编译器优化等)。
 
-   synchronized关键字 (内置锁/监视器锁):-   同步方法: public synchronized void method() { ... }(实例方法锁this,静态方法锁Class对象)。
-   同步代码块: synchronized(obj) { ... }(锁指定的对象obj)。
- 作用: 保证原子性(代码块/方法内同一时刻只有一个线程执行)和可见性(释放锁时会强制将工作内存中的修改刷新到主内存,获取锁时会从主内存读取最新值)。
- 可重入性: 同一个线程可以重复获取自己已经持有的锁。
 
-   同步方法: 
-   volatile关键字:- 作用: 保证变量的可见性和禁止指令重排序。
-   原理: 写入 volatile变量时,会立即将工作内存中的值刷新到主内存。读取volatile变量时,会从主内存读取最新值。
-   不保证原子性! volatile int count = 0; count++;这个自增操作在多线程下仍然是不安全的。
-   适用场景: 状态标志位 (如 volatile boolean shutdownRequested;),单次安全发布的double-checked locking(需要结合synchronized保证初始化原子性)。
 
- 
java.util.concurrent.locks.Lock接口 (显式锁):-   核心实现: ReentrantLock(可重入锁)。
-   优势 (相比 synchronized):-   更灵活:尝试非阻塞获取锁 (tryLock())、可中断的获取锁 (lockInterruptibly())、超时获取锁 (tryLock(long, TimeUnit))。
- 公平锁/非公平锁策略 (构造参数指定)。
-   可以绑定多个 Condition(更精细的线程通信)。
 
-   更灵活:尝试非阻塞获取锁 (
- 
基本用法: 
 Lock lock = new ReentrantLock(); ... lock.lock(); // 获取锁 try { // 访问共享资源 } finally { lock.unlock(); // 务必在 finally 块中释放锁! }
 
-   核心实现: 
- 
java.util.concurrent.atomic包 (原子类):-   核心类: AtomicInteger,AtomicLong,AtomicBoolean,AtomicReference,AtomicIntegerArray,AtomicLongFieldUpdater等。
-   原理: 利用 CAS (Compare-And-Swap) 处理器指令实现无锁的原子操作 (底层 sun.misc.Unsafe类)。
- 优点: 高性能 (在低/中度竞争下),避免锁开销。
-   常用方法: get(),set(),getAndSet(newValue),compareAndSet(expect, update)(核心CAS操作),getAndIncrement(),incrementAndGet(),getAndAdd(delta),addAndGet(delta)。
- 适用场景: 简单的计数器、状态标志、对象引用的原子更新等。
 
-   核心类: 
 
-   线程安全问题根源:
- 
实践练习: -  经典问题: 实现一个多线程累加计数器,观察不加同步 (synchronized,Lock, 原子类) 时的错误结果。
-  分别用 synchronized(方法和代码块)、ReentrantLock、AtomicInteger解决上述计数器问题。比较代码风格和简单性能测试 (注意:简单测试可能不准确,了解差异即可)。
-  编写一个 volatile状态标志的例子:一个线程循环执行任务直到另一个线程将volatile boolean标志置为false。
-  演示 volatile不保证原子性:多个线程对volatile int进行大量自增操作,观察结果是否小于预期总和。
-  练习 ReentrantLock的tryLock(),lockInterruptibly(), 公平锁/非公平锁 (构造一个线程按顺序获取锁的场景观察差异)。
-  练习 AtomicInteger的getAndIncrement,compareAndSet。
 
-  经典问题: 实现一个多线程累加计数器,观察不加同步 (
- 
关键细节 & 坑: -   锁的范围: synchronized锁住的是对象,不是代码。选择最小粒度的锁对象 (this, 特定实例,Class对象)。
- 死锁风险: 锁嵌套使用不当容易导致死锁 (后续阶段重点讲)。
-   synchronized性能: 早期版本重量级,现代JVM优化后性能已很好,除非极端场景,synchronized通常是首选。不要过早优化!先保证正确性!
-   Lock必须手动释放: 务必在finally块中调用unlock(),否则可能导致锁泄漏和死锁。
-   volatile陷阱: 误以为它能解决所有原子性问题。它只解决可见性和有序性,不解决复合操作的原子性 (如i++)。
-   理解 CAS 的 ABA 问题: 虽然原子类的常见方法封装避免了这个问题,但理解 AtomicStampedReference/AtomicMarkableReference解决 ABA 问题的原理有助深入理解。
-   可见性是基础! synchronized和volatile都保证了可见性,这是它们能工作的前提。理解JMM (Java Memory Model)的happens-before原则是进阶关键。
 
-   锁的范围: 
阶段 3:线程通信 (协调工作)
- 目标: 掌握线程间如何协作,等待特定条件满足。
- 
核心知识点: - 
Object的wait(),notify(),notifyAll():-   前提: 必须在 synchronized同步块或方法内部调用!否则会抛IllegalMonitorStateException。
-   wait(): 释放当前持有的锁,使当前线程进入WAITING状态,等待被唤醒。
-   notify(): 唤醒在此对象锁上等待的单个线程 (选择是任意的)。
-   notifyAll(): 唤醒在此对象锁上等待的所有线程。
- 
经典模式 (生产者-消费者基础): 
 synchronized(lock) { while (!condition) { // 必须用 while 循环检查条件!防止虚假唤醒 (Spurious Wakeup) lock.wait(); // 释放 lock 锁,等待 } // 条件满足,执行任务... } synchronized(lock) { // 改变条件... lock.notifyAll(); // 或 lock.notify() }
 
-   前提: 必须在 
- 
Condition接口 (与Lock配合):-   创建: Condition cond = lock.newCondition();
-   方法: await()(类似wait()),signal()(类似notify()),signalAll()(类似notifyAll())。
-   优势:
-   一个 Lock可以关联多个Condition,实现更精细的等待/通知 (例如生产者只唤醒消费者,消费者只唤醒生产者)。
-   避免了 Object的wait/notify必须和synchronized绑定的限制。
 
-   一个 
-   同样需要使用 while循环检查条件防止虚假唤醒!
- 
基本模式: 
 lock.lock(); try { while (!condition) { cond.await(); // 释放锁并等待 } // 执行任务... } finally { lock.unlock(); } lock.lock(); try { // 改变条件... cond.signal(); // 或 cond.signalAll() } finally { lock.unlock(); }
 
-   创建: 
 
- 
- 
实践练习: -  经典生产者-消费者问题 (初级): 使用 synchronized+wait()/notifyAll()实现固定容量的缓冲区 (例如一个长度为1的队列)。一个线程生产数据放入缓冲区,另一个线程从缓冲区取出数据消费。缓冲区满时生产者等待,空时消费者等待。
-  生产者-消费者问题 (升级): 使用 ReentrantLock+Condition实现。创建两个Condition:notFull(不满条件) 和notEmpty(不空条件)。生产者等待notFull,生产后唤醒等待notEmpty的消费者;消费者等待notEmpty,消费后唤醒等待notFull的生产者。体验更精细的控制。
-  在上述练习中,故意去掉 while循环,只保留if,并尝试制造虚假唤醒的场景 (虽然不容易模拟,但理解设计意图)。
 
-  经典生产者-消费者问题 (初级): 使用 
- 
关键细节 & 坑: -   虚假唤醒 (Spurious Wakeup): 线程有可能在没有被 notify/signal、中断或超时的情况下醒来。因此,条件检查必须放在while循环中,而不是if语句中! 这是必须遵守的编程范式。
-   notify()vsnotifyAll():notify()只唤醒一个等待线程,如果唤醒的是“同类”线程 (比如生产者唤醒了另一个生产者),可能无法让程序继续推进。通常更安全、更简单的方式是使用notifyAll()或signalAll(),唤醒所有等待线程,让它们自己去竞争锁并检查条件。使用Condition可以更精细地避免这个问题。
-   锁的持有: 调用 wait()/await()会释放锁,这是线程能协调的关键。调用notify()/signal()时,线程仍然持有锁,被唤醒的线程需要等待当前线程释放锁后才能继续执行。
-   中断处理: wait()和await()可以被中断 (InterruptedException),需要妥善处理中断。
 
-   虚假唤醒 (Spurious Wakeup): 线程有可能在没有被 
阶段 4:并发工具类 (JDK 的瑞士军刀)
- 目标: 掌握常用并发工具类,简化复杂的并发编程任务。
-   核心知识点:
-   CountDownLatch(倒计时闩):- 作用: 允许一个或多个线程等待其他一组线程完成操作。
-   原理: 初始化一个计数器 N。线程调用countDown()使计数器减1。调用await()的线程会阻塞,直到计数器减到0。
- 特点: 计数器不可重置。
- 场景: 主线程等待所有初始化线程完成、并行计算等待所有子任务完成。
 
-   CyclicBarrier(循环栅栏):- 作用: 让一组线程相互等待,直到所有线程都到达某个屏障点,然后一起继续执行 (可选的屏障动作)。
-   原理: 初始化一个参与线程数 N。每个线程调用await()表示到达屏障点并阻塞。当第N个线程调用await()时,所有线程被唤醒继续执行,屏障重置可重用。
- 场景: 分阶段任务,多线程迭代计算。
 
-   Semaphore(信号量):- 作用: 控制同时访问某个特定资源的线程数量 (限流)。
-   原理: 初始化一个许可数 permits。线程调用acquire()获取许可 (若无许可则阻塞),调用release()释放许可。
- 模式: 可以当作互斥锁(许可数为1)或资源池使用。
- 场景: 数据库连接池限流、控制并发下载数。
 
-   BlockingQueue(阻塞队列) 接口:- 作用: 线程安全的队列,支持在队列满时阻塞插入线程,队列空时阻塞移除线程。
-   核心实现:
-   ArrayBlockingQueue: 有界数组队列,FIFO。
-   LinkedBlockingQueue: 可选有界/无界链表队列,FIFO (吞吐量通常更高)。
-   PriorityBlockingQueue: 支持优先级的无界队列。
-   SynchronousQueue: 不存储元素的队列,每个put必须等待一个take,反之亦然 (直接传递)。
-   DelayQueue: 元素按延迟时间排序的无界队列,只有延迟期满的元素才能被取出。
 
-   
-   核心方法:
-   put(e): 阻塞直到队列有空位插入元素。
-   offer(e): 尝试插入,成功返回true,失败返回false(不阻塞)。
-   offer(e, timeout, unit): 限时阻塞插入。
-   take(): 阻塞直到取出队首元素。
-   poll(): 尝试取出,成功返回元素,失败返回null(不阻塞)。
-   poll(timeout, unit): 限时阻塞取出。
 
-   
- 场景: 生产者-消费者模式的最佳实践! 极大简化实现。
 
 
-   
-   实践练习:
-  CountDownLatch: 模拟主线程等待多个加载任务完成后再启动。
-  CyclicBarrier: 模拟赛跑,所有运动员(线程)准备好(await())后一起开跑。多轮比赛 (体现可重用)。
-  Semaphore: 模拟一个只有3个座位的厕所,10个人排队上厕所。
-  BlockingQueue: 用ArrayBlockingQueue或LinkedBlockingQueue重写生产者-消费者问题。体会其简洁性和强大性。尝试使用PriorityBlockingQueue让消费者按优先级消费。
 
-  
-   关键细节 & 坑:
-   CountDownLatch不可重用,CyclicBarrier可重用。
-   CyclicBarrier的屏障动作: 当所有线程到达屏障时,由最后一个进入屏障的线程执行Runnable屏障动作,然后再唤醒所有线程。
-   Semaphore释放许可: 获取多少次许可,最终就应该释放多少次许可 (通常在finally中释放)。
-   BlockingQueue选择:-   需要公平性?(ArrayBlockingQueue构造可选公平策略)。
-   需要无界?(LinkedBlockingQueue默认无界,注意资源耗尽风险) / 需要有界?(ArrayBlockingQueue或LinkedBlockingQueue指定容量)。
-   需要优先级?(PriorityBlockingQueue)。
-   需要直接传递?(SynchronousQueue,常用于Executors.newCachedThreadPool)。
 
-   需要公平性?(
-   理解阻塞与限时: 根据业务需求选择合适的方法 (put/takevsoffer/poll)。
 
-   
阶段 5:线程池 (资源管理的利器)
-   目标: 理解线程池的必要性,掌握 Executor框架的使用和配置。
-   核心知识点:
-   为什么需要线程池?
- 降低资源消耗 (减少线程创建销毁开销)。
- 提高响应速度 (任务到达可直接执行)。
- 提高线程的可管理性 (统一分配、调优、监控)。
 
-   Executor框架 (核心接口):-   Executor: 最基础的执行任务接口 (void execute(Runnable command)).
-   ExecutorService: 扩展Executor,提供生命周期管理 (shutdown(),shutdownNow(),isShutdown(),isTerminated(),awaitTermination()) 和异步任务提交 (submit()返回Future) 能力。
-   ScheduledExecutorService: 扩展ExecutorService,支持定时及周期性任务 (schedule(),scheduleAtFixedRate(),scheduleWithFixedDelay())。
-   Executors: 工厂类,提供创建常用线程池的静态方法 (但需注意其默认配置可能带来的问题!)。
 
-   
-   ThreadPoolExecutor(核心实现类):- 这是你需要深入理解和配置的类。
-   核心构造参数 (七大参数):
-  corePoolSize(核心线程数): 即使空闲也保留的线程数 (除非设置了allowCoreThreadTimeOut)。
-  maximumPoolSize(最大线程数): 线程池允许创建的最大线程数。
-  keepAliveTime(线程空闲存活时间): 当线程数 >corePoolSize时,多余的空闲线程在终止前等待新任务的最长时间。
-  unit(keepAliveTime的时间单位)。
-  workQueue(工作队列): 用于保存等待执行的任务的阻塞队列。常用:LinkedBlockingQueue,ArrayBlockingQueue,SynchronousQueue。
-  threadFactory(线程工厂): 用于创建新线程的工厂 (可以定制线程名、优先级、守护状态等)。
-  handler(拒绝策略): 当线程池已关闭或饱和 (队列满且线程数达maximumPoolSize) 时,处理新提交任务的策略。
 
-  
-   内置拒绝策略:
-   AbortPolicy(默认): 抛出RejectedExecutionException。
-   CallerRunsPolicy: 由提交任务的线程 (调用execute的线程) 自己执行该任务。
-   DiscardPolicy: 静默丢弃无法处理的任务 (不抛异常)。
-   DiscardOldestPolicy: 丢弃工作队列中等待最久 (队头) 的任务,然后尝试重新提交当前任务。
 
-   
 
-   线程池工作流程:
-  提交任务 (execute(Runnable)或submit(Callable/Runnable))。
-  如果当前运行线程数 < corePoolSize,则立即创建新线程执行任务 (即使有空闲线程)。
-  如果运行线程数 >= corePoolSize,则尝试将任务放入工作队列。
-  如果队列已满,且运行线程数 < maximumPoolSize,则创建新线程执行任务。
-  如果队列已满且运行线程数已达 maximumPoolSize,则根据设定的拒绝策略处理该任务。
-  当一个线程空闲时间超过 keepAliveTime且当前线程数 >corePoolSize,则该线程将被终止。
 
-  提交任务 (
-   Executors工厂创建常用池 (了解,但生产环境慎用默认值):-   newFixedThreadPool(int nThreads): 固定大小线程池 (core=max,使用无界LinkedBlockingQueue)。队列无界风险: 任务积压可能导致OOM。
-   newCachedThreadPool(): 可缓存线程池 (核心为0,最大为Integer.MAX_VALUE,SynchronousQueue,空闲线程60秒回收)。线程数无界风险: 大量并发任务可能创建海量线程导致OOM。
-   newSingleThreadExecutor(): 单线程执行器 (core=max=1,使用无界LinkedBlockingQueue)。队列无界风险。保证任务顺序执行。
-   newScheduledThreadPool(int corePoolSize): 定时/周期任务线程池。
 
-   
-   最佳实践:
-   强烈建议直接使用 ThreadPoolExecutor构造方法创建线程池! 明确指定corePoolSize,maxPoolSize, 有界工作队列 (new ArrayBlockingQueue<>(capacity)或new LinkedBlockingQueue<>(capacity)), 合理的拒绝策略 (如CallerRunsPolicy或自定义记录日志/降级)。
-   使用 ThreadFactory定制线程名称 (方便日志排查问题)。
-   监控线程池状态 (通过 ThreadPoolExecutor提供的方法如getPoolSize(),getActiveCount(),getQueue().size(),getCompletedTaskCount()等)。
 
-   强烈建议直接使用 
 
-   为什么需要线程池?
-   实践练习:
-  使用 Executors.newFixedThreadPool执行一组任务。
-  使用 ThreadPoolExecutor手动创建一个线程池:核心=2,最大=4,队列容量=10,拒绝策略=AbortPolicy。模拟大量任务提交 (超过 2+4+10=16个),观察拒绝策略生效。
-  修改拒绝策略为 CallerRunsPolicy,再次测试,观察提交任务的线程 (如main) 是否执行了被拒绝的任务。
-  使用 ScheduledExecutorService实现定时任务 (5秒后执行) 和周期性任务 (每2秒执行一次,固定速率/固定延迟)。
-  实现一个带线程名称前缀 (MyAppThread-) 的ThreadFactory并用于创建线程池。
 
-  使用 
-   关键细节 & 坑:
-   无界队列 (LinkedBlockingQueue默认) 是 OOM 的常见来源! 任务提交速率远高于处理速率时,队列无限增长导致内存耗尽。务必使用有界队列!
-   maximumPoolSize设置过大 (或newCachedThreadPool默认无界) 也是 OOM 来源! 线程过多消耗大量内存和CPU资源。合理评估系统资源。
-   理解线程池工作流程是配置和调优的基础。 特别是队列满时才创建新线程到 maxPoolSize。
-   选择合适的拒绝策略至关重要。 AbortPolicy让调用者感知失败;CallerRunsPolicy提供简单的反馈控制;自定义策略常用于记录日志、持久化任务、降级等。
-   shutdown()vsshutdownNow():shutdown()平缓关闭 (执行完已提交任务),shutdownNow()尝试中断所有正在执行的任务并返回未执行任务列表。通常优先使用shutdown()。
-   submit()返回Future,可以获取任务执行结果或异常。execute()只提交Runnable,不关心结果。
 
-   无界队列 (
阶段 6:死锁与避免 (防范于未然)
- 目标: 理解死锁产生条件,掌握诊断和避免死锁的方法。
-   核心知识点:
-   死锁条件 (Coffman 条件,缺一不可):
- 互斥 (Mutual Exclusion): 资源一次只能被一个线程占用。
- 持有并等待 (Hold and Wait): 一个线程持有至少一个资源,并在等待获取其他线程持有的资源。
- 不可剥夺 (No Preemption): 线程已获得的资源在未使用完之前,不能被强行剥夺。
- 循环等待 (Circular Wait): 存在一组线程 T1, T2, ..., Tn,T1 等待 T2 占有的资源,T2 等待 T3 占有的资源,...,Tn 等待 T1 占有的资源。
 
-   死锁诊断:
-   jstack <pid>: 获取Java进程的线程堆栈快照。分析输出,查找"deadlock"关键字和线程状态、持有的锁、等待的锁信息。是最常用的方法。
- JConsole / VisualVM: 图形化工具,有检测死锁的功能。
 
-   
-   死锁避免策略:
- 破坏“持有并等待”: 一次性申请所有需要的资源 (降低并发度)。
- 破坏“不可剥夺”: 申请更高优先级资源时,若申请不到,主动释放已持有资源 (实现复杂,可能导致活锁)。
-   破坏“循环等待”: 最常用且可行!
- 锁顺序化 (Lock Ordering): 定义全局的锁获取顺序,所有线程都严格按照这个顺序申请锁。例如,有锁 A 和 B,规定必须先拿 A 再拿 B。这样就不会出现线程1持有A等B,线程2持有B等A的循环。
-   锁超时 (tryLock): 使用Lock.tryLock(timeout)尝试获取锁,如果在指定时间内获取失败,则释放自己持有的所有锁,回退并重试 (可能需要随机等待避免活锁)。这不是严格避免死锁,而是从死锁状态中恢复。
 
-   使用更高级别的抽象: 尽量使用 java.util.concurrent包中的并发工具类 (如BlockingQueue,ConcurrentHashMap, 并发工具类等),它们内部经过良好设计,减少了显式锁的使用,降低了死锁概率。
 
 
-   死锁条件 (Coffman 条件,缺一不可):
-   实践练习:
-  制造死锁: 编写两个线程,分别以不同的顺序获取两把锁 (lockA,lockB),制造经典的循环等待死锁。
-  使用 jstack诊断: 运行死锁程序,使用jps找到进程ID,再用jstack <pid>导出堆栈,分析死锁报告。
-  破坏死锁 (锁顺序化): 修改上面的死锁程序,强制两个线程按照相同的全局顺序 (例如先 lockA后lockB) 获取锁。
-  破坏死锁 (锁超时): 修改程序,使用 tryLock(timeout)尝试获取第二把锁。如果超时获取失败,则释放第一把锁,等待随机时间后重试整个操作。
 
-  制造死锁: 编写两个线程,分别以不同的顺序获取两把锁 (
-   关键细节 & 坑:
- 理解四个条件缺一不可。 破坏其中任意一个即可避免死锁。
- 锁顺序化是最推荐的方法,但需要良好的设计和对所有锁的全局认知。 在大型系统中维护全局顺序可能比较困难。
-   锁超时/重试策略:
- 需要仔细设计回退和重试逻辑,避免活锁 (线程不断重试失败)。
- 设置合理的超时时间。
-   重试时最好加入随机退避 (Thread.sleep(random))。
 
- 避免嵌套锁: 尽量减少锁的嵌套层级。
- 缩小锁范围: 只在绝对必要的地方加锁,持有锁的时间尽可能短。
 
阶段 7:实战练习 (融会贯通)
- 目标: 综合运用所学知识解决实际问题,加深理解。
-   推荐练习:
-  生产者-消费者 (高级):
-   使用 BlockingQueue实现。
- 支持多生产者、多消费者。
- 支持生产/消费不同速度。
-   加入优雅关闭机制 (如发送“毒丸” Poison Pill对象通知消费者结束)。
 
-   使用 
-  并发计数器:
-   比较 synchronized,ReentrantLock,AtomicLong,LongAdder(JDK8+) 的性能和适用场景 (特别是高并发写)。理解LongAdder的分段思想。
 
-   比较 
-  并发转账:
-   模拟银行账户转账 (Account类有balance)。
- 关键问题: 如何避免死锁?(锁顺序化 - 按账户ID排序锁)。
-   实现 transfer(Account from, Account to, int amount)方法。
- 考虑并发性能和正确性。
 
-   模拟银行账户转账 (
-  Web服务器请求处理模拟:
-   使用线程池 (ThreadPoolExecutor) 处理传入的请求 (Runnable)。
- 模拟请求处理耗时。
- 监控线程池状态。
- 实现优雅关闭 (等待已提交任务完成)。
 
-   使用线程池 (
-  缓存实现:
-   实现一个简单的线程安全的缓存 (ConcurrentHashMap做存储)。
- 考虑缓存失效 (定时清理、LRU)。
-   考虑缓存击穿:使用 synchronized+double-check或FutureTask模式保证只有一个线程加载缺失的数据。
 
-   实现一个简单的线程安全的缓存 (
-  使用 CompletableFuture(JDK8+): 学习更现代的异步编程方式,处理复杂的异步任务链和组合 (这是并发学习的自然延伸)。
 
-  生产者-消费者 (高级):
-   关键点:
- 优先保证正确性,再考虑优化性能。
-   善用工具: 日志、jstack, JConsole/VisualVM, 性能剖析工具 (如JProfiler,Async Profiler)。
-   编写并发测试用例: 使用 CountDownLatch/CyclicBarrier制造并发压力,使用Thread.sleep或随机延迟模拟不确定性,多次运行。
- 注意资源清理和优雅关闭。
 
学习建议:
- 循序渐进: 严格按照路线图一步步来,不要跳跃。理解前一阶段是后一阶段的基础。
-  动手!动手!动手! 只看不练等于没学。每个知识点都写代码验证,即使是最简单的 HelloWorld多线程。修改代码,故意制造问题 (如去掉synchronized),观察现象,加深理解。
-  理解原理,而非死记API: 问自己“为什么需要这个?”、“它是怎么工作的?”。理解 synchronized的锁对象、volatile的可见性、CAS的原理、线程池的工作流程、死锁的条件等底层机制至关重要。
-  善用官方文档: java.util.concurrent包的 Javadoc 是宝藏,包含详细的类说明、方法解释和使用示例。Java Concurrency in Practice(虽然有点老,但理论经典) 和Oracle Java Tutorials也是极好的资源。
-  阅读源码: 在掌握基础后,尝试阅读 java.util.concurrent包中常用类 (ReentrantLock,ThreadPoolExecutor,ConcurrentHashMap- JDK8+) 的源码。这是提升的捷径,能学到大师的设计思想和技巧。注意从简单的开始。
-  关注可见性和有序性: 这是并发编程中最容易忽视也最难理解的部分。深入理解 Java Memory Model (JMM)和happens-before规则是成为高手的必经之路 (可以在阶段2后或阶段7时补充学习)。
- 保持耐心: 并发编程是Java学习中的难点,概念抽象,Bug难以复现。遇到困难是正常的,多思考、多实践、多调试。
这个路线图提供了一个相对完整的路径和丰富的细节。学习过程中遇到任何具体问题 (概念不清、代码Bug、练习想法),随时可以提问!祝你学习顺利,征服Java并发!
 

 
    
Top comments (0)