- 并发问题的根源:可见性、原子性与有序性
- JUC核心组件深度剖析
- 原子类
- 锁
- 并发集合
- 并发工具类
- 线程池
- 高级并发编程模式
- Guarded Suspension(保护性暂停)
- Balking(犹豫)模式
- Thread-Per-Message(每消息一线程)模式
- Worker Thread(工作者线程)模式
- Future模式
- Java内存模型与happens-before原则
- 性能调优与最佳实践
并发问题的根源:三大特性
在深入高级应用之前,必须深刻理解并发编程的三大核心问题,它们是所有并发Bug的根源。

-
原子性:一个或多个操作,要么全部执行且执行的过程不会被任何因素打断,要么就都不执行,经典问题:
i++。i++实际上包含三个操作:读取i的值 -> 值加1 -> 写回i,在多线程环境下,如果两个线程同时读取i,然后各自加1,再写回,最终结果只增加了1,而不是期望的2。- 解决方案:
synchronized关键字、java.util.concurrent.atomic包下的原子类。
-
可见性:当一个线程修改了一个共享变量的值,其他线程能够立即得知这个修改,在Java内存模型中,每个线程都有自己的工作内存(高速缓存),线程对变量的操作都在工作内存中进行,线程间无法直接访问对方的工作内存。
- 问题:线程A修改了共享变量X,但修改后的值还未同步到主内存,线程B此时去读取X,读到的就是旧值。
- 解决方案:
volatile关键字(确保变量修改后立即同步到主内存,读取时从主内存读取)、synchronized关键字(在解锁前必须把数据刷新回主内存,在加锁时必须清空工作内存从主内存重新加载)。
-
有序性:即程序执行的顺序按照代码的先后顺序执行,编译器和处理器为了优化性能,可能会对指令进行重排序。
- 问题:在单线程环境下,重排序不会影响最终结果,但在多线程环境下,重排序可能会破坏逻辑,经典的例子就是双重检查锁定实现的单例模式。
- 解决方案:
volatile关键字(可以禁止指令重排序)、synchronized关键字(一个变量在同一个时刻只允许一条线程对其进行lock操作,这使得持有同一个锁的两个同步块只能串行地进入)。
JUC核心组件深度剖析
JUC是Java并发编程的“瑞士军刀”,理解其核心组件是高级应用的基础。

1 原子类
java.util.concurrent.atomic包提供了一系列原子变量类,它们利用了CAS(Compare-And-Swap)操作,在硬件层面保证了原子性,性能通常优于synchronized。
-
基础类型原子类:
AtomicInteger,AtomicLong,AtomicBoolean- 高级应用:
AtomicInteger的updateAndGet,accumulateAndGet等函数式更新方法,可以避免手动加锁,实现复杂的原子更新。// 原子地将i增加10,并返回新值 int newValue = atomicInteger.updateAndGetAndGet(x -> x + 10);
- 高级应用:
-
数组类型原子类:
AtomicIntegerArray,AtomicReferenceArray- 高级应用:保证对数组中某个索引位置的修改是原子的。
-
引用类型原子类:
AtomicReference,AtomicStampedReference,AtomicMarkableReference
(图片来源网络,侵删)- 高级应用:
AtomicReference:可以原子地更新任意对象,适用于实现无锁的数据结构。AtomicStampedReference:解决了CAS的ABA问题,它不仅比较引用值,还比较一个“版本戳”(stamp),即使引用值从A变成B又变回A,只要版本戳不同,CAS操作也会失败。// 解决ABA问题的示例 AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0); int stamp = ref.getStamp(); // 线程1执行 ref.compareAndSet("A", "B", stamp, stamp + 1); // ... 线程2可能已经将值改回了"A",并更新了stamp // 线程1再次执行 ref.compareAndSet("B", "A", stamp, stamp + 1); // 这会失败,因为stamp不匹配
- 高级应用:
2 锁
synchronized是Java内置的悲观锁,而JUC提供了更灵活、功能更丰富的锁。
-
ReentrantLock(可重入锁)
-
高级特性:
- 可中断的获取锁:
lockInterruptibly(),如果一个线程在获取锁时被中断,它会抛出InterruptedException,并立即释放资源,而不是像synchronized那样一直等待。 - 公平锁/非公平锁:构造函数可以指定,公平锁按照请求的顺序获取锁,性能较差;非公平锁允许“插队”,吞吐量更高。
- 锁超时:
tryLock()和tryLock(long time, TimeUnit unit),可以尝试获取锁,如果获取失败或超时,就立即返回,避免无限等待。 - 多个条件变量:一个
ReentrantLock可以绑定多个Condition对象,而synchronized只有一个等待队列,这可以实现更精细的线程唤醒控制。ReentrantLock lock = new ReentrantLock(); Condition conditionA = lock.newCondition(); Condition conditionB = lock.newCondition();
// 线程A lock.lock(); try { conditionA.await(); // 等待条件A } finally { lock.unlock(); }
- 可中断的获取锁:
-
-
ReadWriteLock(读写锁)
- 高级应用:适用于“读多写少”的场景,它允许多个读线程同时访问共享资源,但写线程是独占的。
- 锁降级:遵循“获取写锁 -> 获取读锁 -> 释放写锁”的顺序,可以安全地实现锁降级。
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); rwLock.writeLock().lock(); try { // 写操作... rwLock.readLock().lock(); // 锁降级 try { // 读操作... } finally { rwLock.readLock().unlock(); } } finally { rwLock.writeLock().unlock(); }
-
StampedLock(邮戳锁)
- 高级应用:Java 8引入,是
ReadWriteLock的增强版,提供了三种模式的锁:写锁、悲观读锁、乐观读。 - 乐观读:在完全没有锁竞争的情况下,它的性能比
ReadWriteLock的读锁高得多,适用于“读操作远多于写操作,且读操作之间不互相影响”的场景。 - 工作流程:先尝试进行乐观读(
tryOptimisticRead()),获取一个“邮戳”,然后执行读操作,最后验证邮戳是否有效(validate(stamp)),如果无效,说明在此期间有写线程修改了数据,此时需要升级为悲观读锁重新读取。StampedLock lock = new StampedLock(); long stamp = lock.tryOptimisticRead(); // 读取数据... if (!lock.validate(stamp)) { // 检查是否被写线程修改 stamp = lock.readLock(); // 升级为悲观读锁 try { // 重新读取数据... } finally { lock.unlockRead(stamp); } }
- 高级应用:Java 8引入,是
3 并发集合
-
ConcurrentHashMap
- 高级应用:它是线程安全的HashMap,性能极高,其实现经历了多个版本:
- JDK 1.7:分段锁,将数据分成多个段,每个段有自己的锁,多线程访问不同段的数据时不会冲突,并发度取决于段的数量。
- JDK 1.8+:CAS +
synchronized,取消了分段锁,改为对数组中的每个元素(Node)加锁,当发生哈希冲突时,只在链表或红黑树的第一个节点上加锁,大大减小了锁的粒度,提高了并发性能。
- 高级应用:它是线程安全的HashMap,性能极高,其实现经历了多个版本:
-
CopyOnWriteArrayList / CopyOnWriteArraySet
- 高级应用:写时复制,任何修改操作(add, set, remove)都会创建一个底层数组的新副本,在副本上进行修改,然后替换掉旧的引用。
- 适用场景:读多写少的场景,因为读操作完全不加锁,性能很高,但写操作开销大,且可能读到旧数据。
- 典型应用:事件监听器列表、
Observable模式的实现。
-
BlockingQueue(阻塞队列)
- 高级应用:生产者-消费者模式的最佳实践,当队列为空时,消费者线程会自动阻塞;当队列满时,生产者线程会自动阻塞。
- 核心实现:
ArrayBlockingQueue:基于数组的有界阻塞队列,必须指定容量。LinkedBlockingQueue:基于链表的可选有界阻塞队列,默认容量为Integer.MAX_VALUE,吞吐量通常高于ArrayBlockingQueue。SynchronousQueue:一个不存储元素的阻塞队列,每个put操作必须等待一个take操作,反之亦然,常用于直接传递的场景。PriorityBlockingQueue:支持优先级的无界阻塞队列。
4 并发工具类
-
CountDownLatch(倒计时门闩)
- 高级应用:让一个或多个线程等待一组事件完成,它初始化一个计数器,每个线程完成任务后调用
countDown(),当计数器归零时,所有在await()上等待的线程才会被唤醒。 - 场景:主线程等待多个子线程执行完毕。
- 高级应用:让一个或多个线程等待一组事件完成,它初始化一个计数器,每个线程完成任务后调用
-
CyclicBarrier(循环栅栏)
- 高级应用:让一组线程在到达某个屏障时阻塞,直到所有线程都到达屏障,然后再一起继续执行。
CyclicBarrier可以重复使用。 - 高级特性:可以传入一个
Runnable任务,当所有线程到达屏障时,该任务会优先执行。 - 场景:多线程分阶段计算,每个阶段都需要所有线程都准备好才能进入下一阶段。
- 高级应用:让一组线程在到达某个屏障时阻塞,直到所有线程都到达屏障,然后再一起继续执行。
-
Semaphore(信号量)
- 高级应用:控制同时访问某个特定资源的线程数量,它像一个停车场的许可证。
- 场景:数据库连接池、限制系统访问流量。
-
Exchanger(交换器)
- 高级应用:用于两个线程之间交换数据,当一个线程调用
exchange()方法时,它会阻塞,直到另一个线程也调用了exchange()方法,然后两个线程会交换各自的数据。 - 场景:遗传算法中交换个体、工作线程间交换任务。
- 高级应用:用于两个线程之间交换数据,当一个线程调用
5 线程池
线程池是管理线程生命周期的核心工具,避免频繁创建和销毁线程带来的开销。
-
核心参数:
corePoolSize:核心线程数,即使空闲,也会一直存活。maximumPoolSize:最大线程数,当任务队列满了,且线程数小于maximumPoolSize时,会创建新线程。keepAliveTime:空闲线程存活时间,当线程数超过corePoolSize时,多余的空闲线程在等待新任务的最长时间。workQueue:任务队列,用于存放等待执行的任务。threadFactory:线程工厂,用于创建新线程,可以自定义线程名、优先级等。RejectedExecutionHandler:拒绝策略,当线程数和队列都满了时的处理策略。
-
内置线程池:
Executors.newFixedThreadPool(n):固定大小线程池,核心数=最大数,队列无界,容易导致OOM。Executors.newCachedThreadPool():可缓存线程池,核心数为0,最大数很大,空闲线程60秒后回收,适合处理大量短任务,但可能导致系统资源耗尽。Executors.newSingleThreadExecutor():单线程线程池,保证任务按顺序执行。Executors.newScheduledThreadPool(n):定时任务线程池,可以延迟或定期执行任务。
-
高级应用:
ThreadPoolExecutor- 最佳实践:强烈建议使用
ThreadPoolExecutor的构造方法来创建线程池,而不是Executors工具类,这样可以明确控制队列大小,避免OOM风险。 - 自定义拒绝策略:可以实现
RejectedExecutionHandler接口,根据业务需求处理被拒绝的任务,如记录日志、重试等。 - 优雅关闭:调用
shutdown()(停止接受新任务,处理完已提交任务)或shutdownNow()(尝试停止所有正在执行的任务,并返回等待执行的任务列表),配合awaitTermination()可以实现关闭超时控制。
- 最佳实践:强烈建议使用
高级并发编程模式
掌握这些模式可以让你更好地设计并发程序。
-
Guarded Suspension(保护性暂停)
- 模式:一个线程(Guardian)等待一个特定条件满足,另一个线程(Modifier)修改该条件并通知等待的线程。
- JUC实现:
wait()/notify()或Condition的await()/signal()。 - 示例:
Future模式中的get()方法,如果任务还没完成,调用get()的线程就会阻塞。
-
Balking(犹豫)模式
- 模式:当某个操作发现当前状态不满足执行条件时,直接放弃,而不是等待。
- 示例:
java.io.Properties的load()方法,如果文件已经被加载过,再次调用load()会直接返回。
-
Thread-Per-Message(每消息一线程)
- 模式:为每个消息(任务)分配一个独立的线程来处理。
- 实现:直接创建新线程,缺点是线程创建开销大,资源消耗多。
- JUC实现:
Executors.newCachedThreadPool()是对此模式的优化实现。
-
Worker Thread(工作者线程)
- 模式:有一个任务队列和一组固定的工作线程,工作线程从队列中取出任务并执行。
- JUC实现:这正是
ThreadPoolExecutor的核心工作模式。
-
Future模式
- 模式:异步调用,主线程提交一个任务后,立即得到一个
Future对象,它可以用来检查任务是否完成、获取结果或取消任务,而不会阻塞主线程。 - JUC实现:
Future接口:代表一个异步计算的未来结果。FutureTask:Future接口的实现类,同时也是一个Runnable,可以提交给线程池执行。CompletableFuture(Java 8+):Future的超级升级版,支持函数式编程,可以方便地组合多个异步任务,处理异常和结果转换,极大简化了异步编程的复杂性。CompletableFuture.supplyAsync(() -> "Hello") .thenApply(s -> s + " World") .thenAccept(System.out::println); // 输出 "Hello World"
- 模式:异步调用,主线程提交一个任务后,立即得到一个
Java内存模型与happens-before原则
JMM是一套规范,定义了线程和主内存之间的抽象关系,以及哪些操作是可见的、有序的。
-
核心目的:在跨线程操作时,对内存的访问进行一些必要的限制,以实现并发程序的正确性。
-
happens-before原则:是判断内存可见性的重要依据,如果两个操作之间存在happens-before关系,那么前一个操作的结果对后一个操作就是可见的。
- 程序次序规则:在一个线程内,书写在前面的代码happens-before书写在后面的代码。
- 管程锁定规则:一个unlock操作happens-before后面对同一个锁的lock操作。
- volatile变量规则:对一个volatile变量的写操作happens-before后面对这个变量的读操作。
- 线程启动规则:线程的
start()方法happens-before于此线程的每一个动作。 - 线程终止规则:线程中的所有操作都happens-before对此线程的终止检测(例如
Thread.join())。 - 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
性能调优与最佳实践
- 避免过度同步:同步会带来性能开销,尽量使用无锁算法(如CAS)或更细粒度的锁。
- 缩小同步块范围:不要对整个方法进行同步,只同步必要的代码块。
- 优先使用JUC工具:优先使用
ConcurrentHashMap、BlockingQueue等经过高度优化的并发工具,而不是自己用synchronized实现。 - 警惕死锁:避免多个线程以不同的顺序获取多个锁,如果必须,可以按照固定的顺序获取锁,或者使用
tryLock设置超时。 - 使用线程池:避免为每个任务都创建一个新线程。
- 考虑并发级别:对于
ConcurrentHashMap、CopyOnWriteArrayList等,根据硬件CPU核心数设置合理的并发级别。 - 使用线程安全类:优先使用不可变对象(如
String)或线程安全类来减少同步的需要。 - 善用并发工具:
CountDownLatch、CyclicBarrier等工具能让你的并发代码更清晰、更健壮。 - 测试与监控:使用压力测试工具(如JMeter)模拟高并发场景,使用JVM工具(如
jstack)分析线程死锁、jconsole或VisualVM监控线程状态和CPU使用情况。
Java多线程与并发库的高级应用是一个系统性工程,它要求开发者不仅会用API,更要理解其底层原理(JMM、CAS、锁机制)和设计模式,从原子类、锁、并发集合到线程池,JUC为我们提供了强大的武器库,在实际开发中,应遵循“组合优于继承,优于实现”的原则,优先使用并发工具,编写出既安全又高效的并发程序。
