DEV Community

Liu yu
Liu yu

Posted on

Java并发详细学习路线

核心理念:理解问题 -> 学习工具 -> 动手实践 -> 深入原理


详细学习路线 & 阶段指南:

阶段 1:线程基础与生命周期 (打好根基)

  • 目标: 理解什么是线程,如何在Java中创建和启动线程,线程的生命周期状态及其转换。
  • 核心知识点:
    • Thread 类:
      • 继承 Thread 类并重写 run() 方法。
      • 创建 Thread 实例,调用 start() 方法启动线程 (理解 start()run() 的区别!)。
      • 线程命名 (setName(), getName())。
    • Runnable 接口:
      • 实现 Runnable 接口并实现 run() 方法。
      • Runnable 实例传递给 Thread 构造函数 (推荐方式,更灵活,避免单继承限制)。
      • 理解 ThreadRunnable 的关系:Thread 本身也是 Runnable
    • 线程生命周期:
      • NEW: 创建后尚未 start()
      • RUNNABLE: 调用 start() 后,在JVM中等待CPU时间片或正在运行。(注意:操作系统层面的 RunningReady 在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退出,守护线程会被强制终止)。
  • 实践练习:
    1. 分别用继承 Thread 和实现 Runnable 创建多个线程,打印不同信息。
    2. 观察 start() 和直接调用 run() 的区别。
    3. 使用 sleep() 模拟耗时操作,观察线程状态 (Thread.getState())。
    4. 练习 join():主线程等待子线程结束再继续。
    5. 练习 interrupt() 和中断处理:在 run() 方法中循环检查 Thread.interrupted()isInterrupted(),收到中断请求后优雅退出循环。
    6. 创建守护线程和非守护线程,观察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)
      • 适用场景: 简单的计数器、状态标志、对象引用的原子更新等。
  • 实践练习:

    1. 经典问题: 实现一个多线程累加计数器,观察不加同步 (synchronized, Lock, 原子类) 时的错误结果。
    2. 分别用 synchronized (方法和代码块)、ReentrantLockAtomicInteger 解决上述计数器问题。比较代码风格和简单性能测试 (注意:简单测试可能不准确,了解差异即可)。
    3. 编写一个 volatile 状态标志的例子:一个线程循环执行任务直到另一个线程将 volatile boolean 标志置为 false
    4. 演示 volatile 不保证原子性:多个线程对 volatile int 进行大量自增操作,观察结果是否小于预期总和。
    5. 练习 ReentrantLocktryLock(), lockInterruptibly(), 公平锁/非公平锁 (构造一个线程按顺序获取锁的场景观察差异)。
    6. 练习 AtomicIntegergetAndIncrement, compareAndSet
  • 关键细节 & 坑:

    • 锁的范围: synchronized 锁住的是对象,不是代码。选择最小粒度的锁对象 (this, 特定实例,Class 对象)。
    • 死锁风险: 锁嵌套使用不当容易导致死锁 (后续阶段重点讲)。
    • synchronized 性能: 早期版本重量级,现代JVM优化后性能已很好,除非极端场景,synchronized 通常是首选。不要过早优化!先保证正确性!
    • Lock 必须手动释放: 务必在 finally 块中调用 unlock(),否则可能导致锁泄漏和死锁。
    • volatile 陷阱: 误以为它能解决所有原子性问题。它只解决可见性和有序性,不解决复合操作的原子性 (如 i++)。
    • 理解 CAS 的 ABA 问题: 虽然原子类的常见方法封装避免了这个问题,但理解 AtomicStampedReference/AtomicMarkableReference 解决 ABA 问题的原理有助深入理解。
    • 可见性是基础! synchronizedvolatile 都保证了可见性,这是它们能工作的前提。理解 JMM (Java Memory Model)happens-before 原则是进阶关键。

阶段 3:线程通信 (协调工作)

  • 目标: 掌握线程间如何协作,等待特定条件满足。
  • 核心知识点:

    • Objectwait(), 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,实现更精细的等待/通知 (例如生产者只唤醒消费者,消费者只唤醒生产者)。
        • 避免了 Objectwait/notify 必须和 synchronized 绑定的限制。
      • 同样需要使用 while 循环检查条件防止虚假唤醒!
      • 基本模式:

        lock.lock();
        try {
            while (!condition) {
                cond.await(); // 释放锁并等待
            }
            // 执行任务...
        } finally {
            lock.unlock();
        }
        
        lock.lock();
        try {
            // 改变条件...
            cond.signal(); // 或 cond.signalAll()
        } finally {
            lock.unlock();
        }
        
  • 实践练习:

    1. 经典生产者-消费者问题 (初级): 使用 synchronized + wait()/notifyAll() 实现固定容量的缓冲区 (例如一个长度为1的队列)。一个线程生产数据放入缓冲区,另一个线程从缓冲区取出数据消费。缓冲区满时生产者等待,空时消费者等待。
    2. 生产者-消费者问题 (升级): 使用 ReentrantLock + Condition 实现。创建两个 ConditionnotFull (不满条件) 和 notEmpty (不空条件)。生产者等待 notFull,生产后唤醒等待 notEmpty 的消费者;消费者等待 notEmpty,消费后唤醒等待 notFull 的生产者。体验更精细的控制。
    3. 在上述练习中,故意去掉 while 循环,只保留 if,并尝试制造虚假唤醒的场景 (虽然不容易模拟,但理解设计意图)。
  • 关键细节 & 坑:

    • 虚假唤醒 (Spurious Wakeup): 线程有可能在没有被 notify/signal、中断或超时的情况下醒来。因此,条件检查必须放在 while 循环中,而不是 if 语句中! 这是必须遵守的编程范式。
    • notify() vs notifyAll() notify() 只唤醒一个等待线程,如果唤醒的是“同类”线程 (比如生产者唤醒了另一个生产者),可能无法让程序继续推进。通常更安全、更简单的方式是使用 notifyAll()signalAll(),唤醒所有等待线程,让它们自己去竞争锁并检查条件。使用 Condition 可以更精细地避免这个问题。
    • 锁的持有: 调用 wait()/await()释放锁,这是线程能协调的关键。调用 notify()/signal() 时,线程仍然持有锁,被唤醒的线程需要等待当前线程释放锁后才能继续执行。
    • 中断处理: wait()await() 可以被中断 (InterruptedException),需要妥善处理中断。

阶段 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): 限时阻塞取出。
      • 场景: 生产者-消费者模式的最佳实践! 极大简化实现。
  • 实践练习:
    1. CountDownLatch: 模拟主线程等待多个加载任务完成后再启动。
    2. CyclicBarrier: 模拟赛跑,所有运动员(线程)准备好(await())后一起开跑。多轮比赛 (体现可重用)。
    3. Semaphore: 模拟一个只有3个座位的厕所,10个人排队上厕所。
    4. BlockingQueue:ArrayBlockingQueueLinkedBlockingQueue 重写生产者-消费者问题。体会其简洁性和强大性。尝试使用 PriorityBlockingQueue 让消费者按优先级消费。
  • 关键细节 & 坑:
    • CountDownLatch 不可重用,CyclicBarrier 可重用。
    • CyclicBarrier 的屏障动作: 当所有线程到达屏障时,由最后一个进入屏障的线程执行 Runnable 屏障动作,然后再唤醒所有线程。
    • Semaphore 释放许可: 获取多少次许可,最终就应该释放多少次许可 (通常在 finally 中释放)。
    • BlockingQueue 选择:
      • 需要公平性?(ArrayBlockingQueue 构造可选公平策略)。
      • 需要无界?(LinkedBlockingQueue 默认无界,注意资源耗尽风险) / 需要有界?(ArrayBlockingQueueLinkedBlockingQueue 指定容量)。
      • 需要优先级?(PriorityBlockingQueue)。
      • 需要直接传递?(SynchronousQueue,常用于 Executors.newCachedThreadPool)。
    • 理解阻塞与限时: 根据业务需求选择合适的方法 (put/take vs offer/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 (核心实现类):
      • 这是你需要深入理解和配置的类。
      • 核心构造参数 (七大参数):
        1. corePoolSize (核心线程数): 即使空闲也保留的线程数 (除非设置了 allowCoreThreadTimeOut)。
        2. maximumPoolSize (最大线程数): 线程池允许创建的最大线程数。
        3. keepAliveTime (线程空闲存活时间): 当线程数 > corePoolSize 时,多余的空闲线程在终止前等待新任务的最长时间。
        4. unit (keepAliveTime 的时间单位)。
        5. workQueue (工作队列): 用于保存等待执行的任务的阻塞队列。常用:LinkedBlockingQueue, ArrayBlockingQueue, SynchronousQueue
        6. threadFactory (线程工厂): 用于创建新线程的工厂 (可以定制线程名、优先级、守护状态等)。
        7. handler (拒绝策略): 当线程池已关闭或饱和 (队列满且线程数达 maximumPoolSize) 时,处理新提交任务的策略。
      • 内置拒绝策略:
        • AbortPolicy (默认): 抛出 RejectedExecutionException
        • CallerRunsPolicy: 由提交任务的线程 (调用 execute 的线程) 自己执行该任务。
        • DiscardPolicy: 静默丢弃无法处理的任务 (不抛异常)。
        • DiscardOldestPolicy: 丢弃工作队列中等待最久 (队头) 的任务,然后尝试重新提交当前任务。
    • 线程池工作流程:
      1. 提交任务 (execute(Runnable)submit(Callable/Runnable))。
      2. 如果当前运行线程数 < corePoolSize,则立即创建新线程执行任务 (即使有空闲线程)。
      3. 如果运行线程数 >= corePoolSize,则尝试将任务放入工作队列
      4. 如果队列已满,且运行线程数 < maximumPoolSize,则创建新线程执行任务。
      5. 如果队列已满且运行线程数已达 maximumPoolSize,则根据设定的拒绝策略处理该任务。
      6. 当一个线程空闲时间超过 keepAliveTime 且当前线程数 > corePoolSize,则该线程将被终止。
    • Executors 工厂创建常用池 (了解,但生产环境慎用默认值):
      • newFixedThreadPool(int nThreads): 固定大小线程池 (core=max,使用无界 LinkedBlockingQueue)。队列无界风险: 任务积压可能导致OOM。
      • newCachedThreadPool(): 可缓存线程池 (核心为0,最大为 Integer.MAX_VALUESynchronousQueue,空闲线程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() 等)。
  • 实践练习:
    1. 使用 Executors.newFixedThreadPool 执行一组任务。
    2. 使用 ThreadPoolExecutor 手动创建一个线程池:核心=2,最大=4,队列容量=10,拒绝策略=AbortPolicy。模拟大量任务提交 (超过 2+4+10=16个),观察拒绝策略生效。
    3. 修改拒绝策略为 CallerRunsPolicy,再次测试,观察提交任务的线程 (如 main) 是否执行了被拒绝的任务。
    4. 使用 ScheduledExecutorService 实现定时任务 (5秒后执行) 和周期性任务 (每2秒执行一次,固定速率/固定延迟)。
    5. 实现一个带线程名称前缀 (MyAppThread-) 的 ThreadFactory 并用于创建线程池。
  • 关键细节 & 坑:
    • 无界队列 (LinkedBlockingQueue 默认) 是 OOM 的常见来源! 任务提交速率远高于处理速率时,队列无限增长导致内存耗尽。务必使用有界队列!
    • maximumPoolSize 设置过大 (或 newCachedThreadPool 默认无界) 也是 OOM 来源! 线程过多消耗大量内存和CPU资源。合理评估系统资源。
    • 理解线程池工作流程是配置和调优的基础。 特别是队列满时才创建新线程到 maxPoolSize
    • 选择合适的拒绝策略至关重要。 AbortPolicy 让调用者感知失败;CallerRunsPolicy 提供简单的反馈控制;自定义策略常用于记录日志、持久化任务、降级等。
    • shutdown() vs shutdownNow(): shutdown() 平缓关闭 (执行完已提交任务),shutdownNow() 尝试中断所有正在执行的任务并返回未执行任务列表。通常优先使用 shutdown()
    • submit() 返回 Future,可以获取任务执行结果或异常。execute() 只提交 Runnable,不关心结果。

阶段 6:死锁与避免 (防范于未然)

  • 目标: 理解死锁产生条件,掌握诊断和避免死锁的方法。
  • 核心知识点:
    • 死锁条件 (Coffman 条件,缺一不可):
      1. 互斥 (Mutual Exclusion): 资源一次只能被一个线程占用。
      2. 持有并等待 (Hold and Wait): 一个线程持有至少一个资源,并在等待获取其他线程持有的资源。
      3. 不可剥夺 (No Preemption): 线程已获得的资源在未使用完之前,不能被强行剥夺。
      4. 循环等待 (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, 并发工具类等),它们内部经过良好设计,减少了显式锁的使用,降低了死锁概率。
  • 实践练习:
    1. 制造死锁: 编写两个线程,分别以不同的顺序获取两把锁 (lockA, lockB),制造经典的循环等待死锁。
    2. 使用 jstack 诊断: 运行死锁程序,使用 jps 找到进程ID,再用 jstack <pid> 导出堆栈,分析死锁报告。
    3. 破坏死锁 (锁顺序化): 修改上面的死锁程序,强制两个线程按照相同的全局顺序 (例如先 lockAlockB) 获取锁。
    4. 破坏死锁 (锁超时): 修改程序,使用 tryLock(timeout) 尝试获取第二把锁。如果超时获取失败,则释放第一把锁,等待随机时间后重试整个操作。
  • 关键细节 & 坑:
    • 理解四个条件缺一不可。 破坏其中任意一个即可避免死锁。
    • 锁顺序化是最推荐的方法,但需要良好的设计和对所有锁的全局认知。 在大型系统中维护全局顺序可能比较困难。
    • 锁超时/重试策略:
      • 需要仔细设计回退和重试逻辑,避免活锁 (线程不断重试失败)。
      • 设置合理的超时时间。
      • 重试时最好加入随机退避 (Thread.sleep(random))。
    • 避免嵌套锁: 尽量减少锁的嵌套层级。
    • 缩小锁范围: 只在绝对必要的地方加锁,持有锁的时间尽可能短。

阶段 7:实战练习 (融会贯通)

  • 目标: 综合运用所学知识解决实际问题,加深理解。
  • 推荐练习:
    1. 生产者-消费者 (高级):
      • 使用 BlockingQueue 实现。
      • 支持多生产者、多消费者。
      • 支持生产/消费不同速度。
      • 加入优雅关闭机制 (如发送“毒丸” Poison Pill 对象通知消费者结束)。
    2. 并发计数器:
      • 比较 synchronized, ReentrantLock, AtomicLong, LongAdder (JDK8+) 的性能和适用场景 (特别是高并发写)。理解 LongAdder 的分段思想。
    3. 并发转账:
      • 模拟银行账户转账 (Account 类有 balance)。
      • 关键问题: 如何避免死锁?(锁顺序化 - 按账户ID排序锁)。
      • 实现 transfer(Account from, Account to, int amount) 方法。
      • 考虑并发性能和正确性。
    4. Web服务器请求处理模拟:
      • 使用线程池 (ThreadPoolExecutor) 处理传入的请求 (Runnable)。
      • 模拟请求处理耗时。
      • 监控线程池状态。
      • 实现优雅关闭 (等待已提交任务完成)。
    5. 缓存实现:
      • 实现一个简单的线程安全的缓存 (ConcurrentHashMap 做存储)。
      • 考虑缓存失效 (定时清理、LRU)。
      • 考虑缓存击穿:使用 synchronized + double-checkFutureTask 模式保证只有一个线程加载缺失的数据。
    6. 使用 CompletableFuture (JDK8+): 学习更现代的异步编程方式,处理复杂的异步任务链和组合 (这是并发学习的自然延伸)。
  • 关键点:
    • 优先保证正确性,再考虑优化性能。
    • 善用工具: 日志、jstack, JConsole/VisualVM, 性能剖析工具 (如 JProfiler, Async Profiler)。
    • 编写并发测试用例: 使用 CountDownLatch/CyclicBarrier 制造并发压力,使用 Thread.sleep 或随机延迟模拟不确定性,多次运行。
    • 注意资源清理和优雅关闭。

学习建议:

  1. 循序渐进: 严格按照路线图一步步来,不要跳跃。理解前一阶段是后一阶段的基础。
  2. 动手!动手!动手! 只看不练等于没学。每个知识点都写代码验证,即使是最简单的 HelloWorld 多线程。修改代码,故意制造问题 (如去掉 synchronized),观察现象,加深理解。
  3. 理解原理,而非死记API: 问自己“为什么需要这个?”、“它是怎么工作的?”。理解 synchronized 的锁对象、volatile 的可见性、CAS 的原理、线程池的工作流程、死锁的条件等底层机制至关重要。
  4. 善用官方文档: java.util.concurrent 包的 Javadoc 是宝藏,包含详细的类说明、方法解释和使用示例。Java Concurrency in Practice (虽然有点老,但理论经典) 和 Oracle Java Tutorials 也是极好的资源。
  5. 阅读源码: 在掌握基础后,尝试阅读 java.util.concurrent 包中常用类 (ReentrantLock, ThreadPoolExecutor, ConcurrentHashMap - JDK8+) 的源码。这是提升的捷径,能学到大师的设计思想和技巧。注意从简单的开始。
  6. 关注可见性和有序性: 这是并发编程中最容易忽视也最难理解的部分。深入理解 Java Memory Model (JMM)happens-before 规则是成为高手的必经之路 (可以在阶段2后或阶段7时补充学习)。
  7. 保持耐心: 并发编程是Java学习中的难点,概念抽象,Bug难以复现。遇到困难是正常的,多思考、多实践、多调试。

这个路线图提供了一个相对完整的路径和丰富的细节。学习过程中遇到任何具体问题 (概念不清、代码Bug、练习想法),随时可以提问!祝你学习顺利,征服Java并发!

Top comments (0)