核心理念:理解问题 -> 学习工具 -> 动手实践 -> 深入原理
详细学习路线 & 阶段指南:
阶段 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/take
vsoffer/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)