Java JUC并发编程全面解析:从原理到实战

文章目录

    • 课程导言
    • 第一部分:JUC概览------Java并发工具包全景
      • [1.1 什么是JUC?](#1.1 什么是JUC?)
      • [1.2 JUC五大核心组件](#1.2 JUC五大核心组件)
      • [1.3 JUC的设计思想](#1.3 JUC的设计思想)
      • [1.4 JUC与synchronized的关系](#1.4 JUC与synchronized的关系)
    • 第二部分:Lock体系与AQS原理(第一课时重点)
      • [2.1 Lock接口概述](#2.1 Lock接口概述)
      • [2.2 ReentrantLock------可重入锁](#2.2 ReentrantLock——可重入锁)
        • [2.2.1 基本用法](#2.2.1 基本用法)
        • [2.2.2 公平锁与非公平锁](#2.2.2 公平锁与非公平锁)
        • [2.2.3 可中断与限时获取](#2.2.3 可中断与限时获取)
        • [2.2.4 synchronized与ReentrantLock对比](#2.2.4 synchronized与ReentrantLock对比)
      • [2.3 ReentrantReadWriteLock------读写锁](#2.3 ReentrantReadWriteLock——读写锁)
        • [2.3.1 概念与原理](#2.3.1 概念与原理)
        • [2.3.2 使用示例](#2.3.2 使用示例)
        • [2.3.3 锁降级](#2.3.3 锁降级)
      • [2.4 AQS------JUC的基石](#2.4 AQS——JUC的基石)
        • [2.4.1 什么是AQS?](#2.4.1 什么是AQS?)
        • [2.4.2 AQS的核心组成](#2.4.2 AQS的核心组成)
        • [2.4.3 AQS的设计模式](#2.4.3 AQS的设计模式)
        • [2.4.4 AQS的工作流程](#2.4.4 AQS的工作流程)
      • [2.5 LockSupport](#2.5 LockSupport)
    • 第三部分:原子类与CAS机制(第一课时重点)
      • [3.1 原子类概述](#3.1 原子类概述)
      • [3.2 为什么需要原子类?](#3.2 为什么需要原子类?)
      • [3.3 CAS原理详解](#3.3 CAS原理详解)
        • [3.3.1 什么是CAS?](#3.3.1 什么是CAS?)
        • [3.3.2 CAS在原子类中的应用](#3.3.2 CAS在原子类中的应用)
        • [3.3.3 CAS的三大缺陷](#3.3.3 CAS的三大缺陷)
      • [3.4 原子类使用示例](#3.4 原子类使用示例)
    • 第四部分:并发工具类(第二课时重点)
      • [4.1 CountDownLatch------倒计数器](#4.1 CountDownLatch——倒计数器)
        • [4.1.1 概念与用途](#4.1.1 概念与用途)
        • [4.1.2 核心方法](#4.1.2 核心方法)
        • [4.1.3 使用示例](#4.1.3 使用示例)
      • [4.2 CyclicBarrier------循环屏障](#4.2 CyclicBarrier——循环屏障)
        • [4.2.1 概念与用途](#4.2.1 概念与用途)
        • [4.2.2 与CountDownLatch的区别](#4.2.2 与CountDownLatch的区别)
        • [4.2.3 使用示例](#4.2.3 使用示例)
      • [4.3 Semaphore------信号量](#4.3 Semaphore——信号量)
        • [4.3.1 概念与用途](#4.3.1 概念与用途)
        • [4.3.2 核心方法](#4.3.2 核心方法)
        • [4.3.3 使用示例](#4.3.3 使用示例)
        • [4.3.4 二元信号量------可替代锁](#4.3.4 二元信号量——可替代锁)
      • [4.4 Exchanger------交换器](#4.4 Exchanger——交换器)
        • [4.4.1 概念与用途](#4.4.1 概念与用途)
        • [4.4.2 使用示例](#4.4.2 使用示例)
    • 第五部分:并发集合(第二课时重点)
      • [5.1 线程安全的集合概述](#5.1 线程安全的集合概述)
      • [5.2 ConcurrentHashMap------并发哈希表](#5.2 ConcurrentHashMap——并发哈希表)
        • [5.2.1 为什么不用Hashtable?](#5.2.1 为什么不用Hashtable?)
        • [5.2.2 JDK 1.7的实现:分段锁](#5.2.2 JDK 1.7的实现:分段锁)
        • [5.2.3 JDK 1.8的演进:CAS + synchronized](#5.2.3 JDK 1.8的演进:CAS + synchronized)
        • [5.2.4 扩容机制优化](#5.2.4 扩容机制优化)
      • [5.3 CopyOnWriteArrayList------写时复制列表](#5.3 CopyOnWriteArrayList——写时复制列表)
        • [5.3.1 原理](#5.3.1 原理)
        • [5.3.2 适用场景](#5.3.2 适用场景)
      • [5.4 BlockingQueue------阻塞队列](#5.4 BlockingQueue——阻塞队列)
        • [5.4.1 概念](#5.4.1 概念)
        • [5.4.2 常用实现类](#5.4.2 常用实现类)
        • [5.4.3 生产者-消费者示例](#5.4.3 生产者-消费者示例)
    • 第六部分:线程池(第二课时重点)
      • [6.1 为什么需要线程池?](#6.1 为什么需要线程池?)
      • [6.2 ThreadPoolExecutor核心参数](#6.2 ThreadPoolExecutor核心参数)
        • [6.2.1 参数详解](#6.2.1 参数详解)
        • [6.2.2 线程池工作流程](#6.2.2 线程池工作流程)
      • [6.3 拒绝策略](#6.3 拒绝策略)
      • [6.4 线程池创建方式](#6.4 线程池创建方式)
        • [6.4.1 通过Executors工厂类](#6.4.1 通过Executors工厂类)
        • [6.4.2 为什么不推荐Executors?](#6.4.2 为什么不推荐Executors?)
        • [6.4.3 正确姿势:直接使用ThreadPoolExecutor](#6.4.3 正确姿势:直接使用ThreadPoolExecutor)
      • [6.5 线程数配置策略](#6.5 线程数配置策略)
      • [6.6 线程池监控](#6.6 线程池监控)
      • [6.7 优雅关闭线程池](#6.7 优雅关闭线程池)
    • 第七部分:JUC优化与实战(第二课时重点)
      • [7.1 锁优化策略](#7.1 锁优化策略)
        • [7.1.1 减少锁持有时间](#7.1.1 减少锁持有时间)
        • [7.1.2 减小锁粒度](#7.1.2 减小锁粒度)
        • [7.1.3 锁分离](#7.1.3 锁分离)
      • [7.2 CAS优化](#7.2 CAS优化)
      • [7.3 并发编程最佳实践](#7.3 并发编程最佳实践)
        • [7.3.1 优先使用并发工具而非手动同步](#7.3.1 优先使用并发工具而非手动同步)
        • [7.3.2 使用不可变对象](#7.3.2 使用不可变对象)
        • [7.3.3 线程封闭](#7.3.3 线程封闭)
        • [7.3.4 避免死锁](#7.3.4 避免死锁)
      • [7.4 性能对比测试](#7.4 性能对比测试)
      • [7.5 常见问题排查](#7.5 常见问题排查)
        • [7.5.1 线程池队列积压](#7.5.1 线程池队列积压)
        • [7.5.2 死锁](#7.5.2 死锁)
        • [7.5.3 内存泄漏](#7.5.3 内存泄漏)
    • 课程总结

课程导言

适用对象

本课程适合已经掌握Java多线程基础知识(如线程创建、synchronized、wait/notify),希望深入学习高并发编程的开发者。无论你是准备面试的求职者,还是希望提升系统性能的一线工程师,这门课程都将为你构建完整的JUC知识体系。

学习目标

通过两个课时的系统学习,你将能够:

  • 掌握 JUC五大核心组件:locks、atomic、tools、collections、executor
  • 理解 AQS抽象同步器的底层原理
  • 熟练使用 ReentrantLock、Semaphore、CountDownLatch等工具类
  • 深入剖析 ConcurrentHashMap的演进与实现
  • 学会 线程池的参数配置与优化策略
  • 掌握 CAS机制及其ABA问题的解决方案

课程安排

  • 第一课时(约60分钟):JUC概览 + Lock体系 + AQS原理 + 原子类与CAS
  • 第二课时(约60分钟):并发工具类 + 并发集合 + 线程池 + 优化与实战

第一部分:JUC概览------Java并发工具包全景

1.1 什么是JUC?

JUC是java.util.concurrent包的简称,是Java专门为支持高并发编程而设计的工具包。在JDK 1.5之后引入,包含了大量用于处理多线程编程的类和接口,可以有效减少竞争条件和死锁问题。

在JUC出现之前,Java的并发编程主要依赖synchronized关键字和wait/notify机制,功能有限且灵活性不足。JUC的诞生标志着Java并发编程进入了一个全新的时代------从"能用"走向"好用"。

1.2 JUC五大核心组件

JUC包的内容虽然丰富,但可以归纳为五大核心组件:

组件 描述 核心类/接口
locks 锁框架,提供更灵活的锁机制 ReentrantLock, ReentrantReadWriteLock, LockSupport
atomic 原子操作类,基于CAS实现 AtomicInteger, AtomicLong, AtomicReference
tools 并发工具类,解决特定同步场景 CountDownLatch, CyclicBarrier, Semaphore
collections 并发集合,线程安全的容器 ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue
executor 线程池框架,管理线程生命周期 ThreadPoolExecutor, Executors, ScheduledExecutorService

1.3 JUC的设计思想

JUC的核心设计思想可以概括为:

  1. 底层依赖 :声明共享变量为volatile,保证可见性
  2. 中层工具:使用CAS进行原子条件更新,实现线程同步
  3. 上层实现:基于AQS(AbstractQueuedSynchronizer)构建各种同步器

这种分层设计使得JUC既保持了高性能,又具备了良好的扩展性。

1.4 JUC与synchronized的关系

很多人误以为JUC是用来取代synchronized的,其实不然。两者是互补关系:

  • synchronized:简单易用,由JVM实现,适合锁竞争不激烈的场景
  • JUC锁:功能丰富,由Java实现,适合需要高级锁特性的场景

在实际开发中,应根据具体需求选择合适的技术。


第二部分:Lock体系与AQS原理(第一课时重点)

2.1 Lock接口概述

Lock接口是JUC锁框架的顶层接口,定义了锁的基本操作:

java 复制代码
public interface Lock {
    void lock();                // 获取锁,获取不到就阻塞
    void lockInterruptibly();   // 可中断地获取锁
    boolean tryLock();          // 尝试获取锁,立即返回
    boolean tryLock(long time, TimeUnit unit); // 限时尝试获取锁
    void unlock();              // 释放锁
    Condition newCondition();   // 创建条件变量
}

相比synchronizedLock提供了三大优势:

  • 可中断lockInterruptibly()允许在等待锁时响应中断
  • 可超时tryLock(timeout)避免无限期等待
  • 可公平:支持公平锁,按请求顺序获取锁

2.2 ReentrantLock------可重入锁

2.2.1 基本用法

ReentrantLockLock接口最经典的实现,使用方式如下:

java 复制代码
public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;
    
    public void increment() {
        lock.lock();  // 获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 必须在finally中释放!
        }
    }
}

核心要点

  • lock()unlock()必须成对出现
  • 解锁必须放在finally块中,确保无论如何都能释放锁
  • 不能在try块中调用lock(),因为lock()本身可能抛出异常
2.2.2 公平锁与非公平锁

ReentrantLock的构造方法可以指定是否公平:

java 复制代码
ReentrantLock fairLock = new ReentrantLock(true);    // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁
ReentrantLock defaultLock = new ReentrantLock();     // 默认非公平
  • 公平锁:线程按照请求锁的先后顺序获取锁,避免饥饿
  • 非公平锁:允许插队,性能更高,但可能导致某些线程永远获取不到锁

为什么默认非公平?因为非公平锁减少了线程挂起和唤醒的开销,在高并发下吞吐量更高。

2.2.3 可中断与限时获取
java 复制代码
public void performTask() throws InterruptedException {
    // 可中断获取锁
    lock.lockInterruptibly();
    try {
        // 执行任务
    } finally {
        lock.unlock();
    }
}

public boolean tryPerform() {
    // 尝试在3秒内获取锁
    try {
        if (lock.tryLock(3, TimeUnit.SECONDS)) {
            try {
                // 成功获取锁
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false; // 获取锁超时
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return false;
    }
}
2.2.4 synchronized与ReentrantLock对比
特性 synchronized ReentrantLock
实现方式 JVM关键字 Java API
锁释放 自动释放 手动解锁
可中断 不支持 支持lockInterruptibly()
超时尝试 不支持 支持tryLock(timeout)
公平性 非公平 可设置公平/非公平
条件变量 一个等待集 多个Condition

选择建议

  • 锁竞争不激烈时,用synchronized,代码简洁
  • 需要公平锁、可中断、超时等待时,用ReentrantLock

2.3 ReentrantReadWriteLock------读写锁

2.3.1 概念与原理

ReentrantReadWriteLock维护了一对锁:

  • 读锁(ReadLock):共享锁,多个读线程可同时持有
  • 写锁(WriteLock):独占锁,只有一个写线程能持有

读写锁遵循三条规则:

  1. 读-读不互斥:多个读线程可并发执行
  2. 读-写互斥:读时不能写,写时不能读
  3. 写-写互斥:多个写线程互斥
2.3.2 使用示例
java 复制代码
public class Cache {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    private Map<String, Object> data = new HashMap<>();
    
    public Object get(String key) {
        readLock.lock();
        try {
            return data.get(key);
        } finally {
            readLock.unlock();
        }
    }
    
    public void put(String key, Object value) {
        writeLock.lock();
        try {
            data.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
}
2.3.3 锁降级

读写锁支持锁降级:写锁可以降级为读锁,但读锁不能升级为写锁。

java 复制代码
writeLock.lock();
try {
    // 修改数据
    data.put(key, value);
    // 在释放写锁前获取读锁,实现降级
    readLock.lock();
} finally {
    writeLock.unlock(); // 释放写锁,但读锁还在
}
try {
    // 此时以读锁执行后续操作
} finally {
    readLock.unlock();
}

2.4 AQS------JUC的基石

2.4.1 什么是AQS?

AQS(AbstractQueuedSynchronizer) 是JUC的核心基础设施,ReentrantLockSemaphoreCountDownLatch等同步器都基于它实现。

AQS的核心思想是:如果请求的共享资源空闲,则将当前线程设置为工作线程;否则将线程加入等待队列

2.4.2 AQS的核心组成

AQS维护了两个关键元素:

  1. volatile int state:同步状态

    • 对于ReentrantLock,state表示锁的持有次数(0表示未持有,≥1表示重入次数)
    • 对于Semaphore,state表示剩余许可数量
    • 对于CountDownLatch,state表示计数器剩余值
  2. FIFO等待队列(CLH队列变体):存放获取资源失败的线程

2.4.3 AQS的设计模式

AQS采用模板方法模式,子类只需实现特定方法:

需要子类实现的方法 描述
tryAcquire(int) 独占式获取资源
tryRelease(int) 独占式释放资源
tryAcquireShared(int) 共享式获取资源
tryReleaseShared(int) 共享式释放资源
isHeldExclusively() 是否独占模式

ReentrantLock为例,它的Sync类继承AQS,实现了tryAcquiretryRelease

2.4.4 AQS的工作流程
复制代码
线程请求资源 → tryAcquire()尝试获取 → 成功则直接执行
                                   ↓ 失败
                             加入等待队列
                                   ↓
                             前驱节点是头节点?→ 是 → 再次尝试获取 → 成功则出队执行
                                   ↓ 否                        ↓ 失败
                                阻塞等待
                                   ↓
                             被唤醒后重复上述过程

2.5 LockSupport

LockSupport是JUC中的基础工具类,提供了线程阻塞和唤醒的原语:

java 复制代码
// 阻塞当前线程
LockSupport.park();

// 唤醒指定线程
LockSupport.unpark(thread);

wait/notify相比,LockSupport的优势在于:

  • 不需要获取锁即可使用
  • 先调用unpark再调用park,线程不会阻塞
  • 可以唤醒指定线程,更精确

第三部分:原子类与CAS机制(第一课时重点)

3.1 原子类概述

java.util.concurrent.atomic包提供了一系列原子操作类:

类型 原子类
基本类型 AtomicInteger, AtomicLong, AtomicBoolean
数组类型 AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray
引用类型 AtomicReference, AtomicStampedReference, AtomicMarkableReference
字段更新器 AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater

3.2 为什么需要原子类?

看一个经典问题:多个线程同时执行count++,最终结果可能小于预期。

java 复制代码
public class UnsafeCounter {
    private int count = 0;
    public void increment() { count++; } // 非原子操作
}

count++不是原子操作,它分解为三步:读→改→写。使用synchronized可以解决,但性能较差。原子类提供了更高效的解决方案:

java 复制代码
public class SafeCounter {
    private AtomicInteger count = new AtomicInteger(0);
    public void increment() { count.incrementAndGet(); }
}

3.3 CAS原理详解

3.3.1 什么是CAS?

CAS(Compare And Swap) 是一条CPU原子指令,包含三个操作数:

  • 内存地址V
  • 期望值A
  • 新值B

核心逻辑:判断内存地址V当前的值是否等于A,如果相等就将V更新为B,整个操作是原子的。

用伪代码表示:

java 复制代码
boolean compareAndSwap(V, A, B) {
    if (V.get() == A) {
        V.set(B);
        return true;
    }
    return false;
}
3.3.2 CAS在原子类中的应用

AtomicIntegerincrementAndGet()为例:

java 复制代码
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// unsafe.getAndAddInt的实现(类似)
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2); // 获取当前值
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); // CAS重试
    return var5;
}

核心就是自旋+CAS:不断尝试,直到成功为止。

3.3.3 CAS的三大缺陷

缺陷一:ABA问题

假设线程1读到变量值为A,此时线程2将A改为B再改回A,线程1进行CAS时发现仍是A,于是更新成功。但实际上变量已经被修改过。

解决方案 :使用AtomicStampedReference,通过版本号/时间戳解决。

java 复制代码
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
int[] stamp = new int[1];
Integer value = ref.get(stamp); // 获取值和版本号
// CAS时同时检查值和版本号
ref.compareAndSet(100, 200, stamp[0], stamp[0] + 1);

缺陷二:循环时间长开销大

如果CAS长时间不成功,会一直自旋,给CPU带来很大开销。

解决方案

  • pause指令,减少CPU消耗
  • 自适应自旋(JVM优化)

缺陷三:只能保证一个共享变量的原子操作

解决方案

  • 将多个变量合并为一个对象,用AtomicReference包装
  • 使用锁

3.4 原子类使用示例

java 复制代码
// 计数器场景
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // +1
count.addAndGet(5);      // +5
count.getAndSet(10);     // 设置为10,返回旧值

// 布尔标志
AtomicBoolean flag = new AtomicBoolean(false);
flag.compareAndSet(false, true); // 原子性修改标志位

// 对象引用
AtomicReference<User> userRef = new AtomicReference<>();
User oldUser = userRef.getAndSet(newUser);

第四部分:并发工具类(第二课时重点)

4.1 CountDownLatch------倒计数器

4.1.1 概念与用途

CountDownLatch允许一个或多个线程等待,直到其他线程执行完一组操作。

生活类比 :跑步比赛,裁判需要等待所有运动员冲过终点才能宣布比赛结束。运动员就是线程,裁判就是CountDownLatch

4.1.2 核心方法
方法 描述
CountDownLatch(int count) 构造器,初始化计数器值
await() 使当前线程等待,直到计数器归零
await(long timeout, TimeUnit unit) 限时等待
countDown() 计数器减1
4.1.3 使用示例
java 复制代码
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        int workerCount = 5;
        CountDownLatch latch = new CountDownLatch(workerCount);
        
        for (int i = 0; i < workerCount; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 开始工作");
                    Thread.sleep((long) (Math.random() * 1000));
                    System.out.println(Thread.currentThread().getName() + " 工作完成");
                    latch.countDown(); // 计数器减1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "工人-" + i).start();
        }
        
        // 主线程等待所有工人完成
        latch.await();
        System.out.println("所有工人完成,主线程继续");
    }
}

典型应用场景

  • 多线程下载大文件,需要等待所有分片下载完成才合并
  • 并行计算,等待所有子任务完成才汇总结果

4.2 CyclicBarrier------循环屏障

4.2.1 概念与用途

CyclicBarrier让一组线程互相等待,直到所有线程都到达某个公共屏障点才继续执行。

生活类比:朋友约好一起去吃饭,约定在餐厅门口集合。所有人都到齐了,才一起进去。这个餐厅门口就是屏障点。

4.2.2 与CountDownLatch的区别
特性 CountDownLatch CyclicBarrier
重用性 不可重用,计数器归零后失效 可重用,通过reset()重置
参与方 等待方和倒计时方分离 所有线程互相等待
计数方式 线程调用countDown()减1 线程调用await()进入等待
4.2.3 使用示例
java 复制代码
public class CyclicBarrierDemo {
    public static void main(String[] args) {
        int threadCount = 5;
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("所有线程已到达屏障,优先执行此任务");
        });
        
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 开始第一阶段");
                    Thread.sleep((long) (Math.random() * 1000));
                    System.out.println(Thread.currentThread().getName() + " 到达屏障");
                    barrier.await(); // 等待其他线程
                    
                    System.out.println(Thread.currentThread().getName() + " 开始第二阶段");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "线程-" + i).start();
        }
    }
}

4.3 Semaphore------信号量

4.3.1 概念与用途

Semaphore是一个计数信号量,用于控制同时访问特定资源的线程数量。

生活类比:停车场门口的电子牌显示剩余车位。每进一辆车,剩余车位减1(P操作);每出一辆车,剩余车位加1(V操作)。没车位时,车辆只能等待。

4.3.2 核心方法
方法 描述
Semaphore(int permits) 构造器,指定许可数量
acquire() 获取许可,没有则阻塞(P操作)
release() 释放许可(V操作)
tryAcquire() 尝试获取许可,立即返回
4.3.3 使用示例
java 复制代码
public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3); // 最多3个线程同时访问
        
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 获取许可,开始工作");
                    Thread.sleep(1000); // 模拟工作
                    System.out.println(Thread.currentThread().getName() + " 释放许可");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "线程-" + i).start();
        }
    }
}
4.3.4 二元信号量------可替代锁

如果将信号量初始化为1,它就相当于一个互斥锁:

java 复制代码
Semaphore mutex = new Semaphore(1);
mutex.acquire(); // lock
try {
    // 临界区
} finally {
    mutex.release(); // unlock
}

4.4 Exchanger------交换器

4.4.1 概念与用途

Exchanger用于两个线程交换数据。当两个线程都到达交换点时,它们会交换彼此的数据。

4.4.2 使用示例
java 复制代码
public class ExchangerDemo {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();
        
        new Thread(() -> {
            try {
                String data1 = "来自线程1的数据";
                System.out.println("线程1准备交换: " + data1);
                String data2 = exchanger.exchange(data1);
                System.out.println("线程1收到: " + data2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "线程1").start();
        
        new Thread(() -> {
            try {
                String data1 = "来自线程2的数据";
                System.out.println("线程2准备交换: " + data1);
                String data2 = exchanger.exchange(data1);
                System.out.println("线程2收到: " + data2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "线程2").start();
    }
}

第五部分:并发集合(第二课时重点)

5.1 线程安全的集合概述

Java中常见的集合大部分都不是线程安全的,如ArrayListHashMapHashSet等。在多线程环境下,需要使用线程安全的替代品。

非线程安全 线程安全(传统) 线程安全(JUC)
HashMap Hashtable ConcurrentHashMap
ArrayList Vector CopyOnWriteArrayList
HashSet - CopyOnWriteArraySet
TreeMap - ConcurrentSkipListMap
TreeSet - ConcurrentSkipListSet
Queue - ConcurrentLinkedQueue

5.2 ConcurrentHashMap------并发哈希表

5.2.1 为什么不用Hashtable?

传统的Hashtable虽然线程安全,但采用全局锁,所有操作都互斥,并发性能极差。而ConcurrentHashMap做了大量优化。

5.2.2 JDK 1.7的实现:分段锁

在JDK 1.7中,ConcurrentHashMap采用分段锁机制:

  • 内部维护一个Segment数组,默认16个

  • 每个Segment继承自ReentrantLock,守护一个HashEntry数组

  • 不同线程操作不同Segment可以并发执行

    ConcurrentHashMap
    ├── Segment 0 (锁) → HashEntry[] → 链表
    ├── Segment 1 (锁) → HashEntry[] → 链表
    ├── ...
    └── Segment 15 (锁) → HashEntry[] → 链表

优点 :理论上支持16个线程并发写(不同Segment)
缺点:并发度受限于Segment数量

5.2.3 JDK 1.8的演进:CAS + synchronized

JDK 1.8对ConcurrentHashMap进行了重构,彻底抛弃了Segment:

  • 数据结构与HashMap一致:数组 + 链表 + 红黑树
  • 线程安全机制:CAS + synchronized
  • 并发粒度从Segment级别细化到桶级别

JDK 1.8的同步策略

  • 若散列桶为空:使用CAS乐观锁,无锁化插入
  • 若散列桶不为空 :对桶的头节点加synchronized,其他桶仍可并发访问
java 复制代码
// JDK 1.8 putVal方法核心逻辑
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // ...
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable(); // 初始化,使用CAS保证线程安全
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 桶为空,CAS插入
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break;
        } else {
            // 桶不为空,对头节点加synchronized锁
            synchronized (f) {
                // 链表或红黑树操作
            }
        }
    }
}
5.2.4 扩容机制优化

JDK 1.8的扩容采用多线程协助扩容机制:

  • 将大数组的扩容拆分为多个小任务
  • 每个线程负责搬运一部分元素到新数组
  • 扩容期间,新老数组共存,查询需要同时查两个数组

这种"化整为零"的扩容方式,极大地减少了扩容对系统的影响。

5.3 CopyOnWriteArrayList------写时复制列表

5.3.1 原理

CopyOnWriteArrayList采用写时复制策略:

  • 读操作无锁,直接读取原数组
  • 写操作时,先复制一份新数组,在新数组上进行修改,最后将原数组引用指向新数组
java 复制代码
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制
        newElements[len] = e;
        setArray(newElements); // 替换引用
        return true;
    } finally {
        lock.unlock();
    }
}
5.3.2 适用场景

优点

  • 读操作无锁,性能极高
  • 读多写少的场景非常适合

缺点

  • 内存占用高:每次写都复制整个数组
  • 数据弱一致性:写操作不能立即被读操作看到

典型应用:黑白名单、监听器列表、缓存等读多写少场景。

5.4 BlockingQueue------阻塞队列

5.4.1 概念

BlockingQueue是支持阻塞操作的队列,常用于生产者-消费者模式。

5.4.2 常用实现类
实现类 特点
ArrayBlockingQueue 基于数组的有界队列,FIFO
LinkedBlockingQueue 基于链表的可选有界队列,FIFO
PriorityBlockingQueue 支持优先级的无界队列
DelayQueue 延迟队列,元素到期才能取出
SynchronousQueue 不存储元素,每个put必须等待take
5.4.3 生产者-消费者示例
java 复制代码
public class ProducerConsumerDemo {
    private static final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
    
    public static void main(String[] args) {
        // 生产者
        new Thread(() -> {
            try {
                for (int i = 0; i < 20; i++) {
                    queue.put(i);
                    System.out.println("生产: " + i);
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        
        // 消费者
        new Thread(() -> {
            try {
                while (true) {
                    Integer value = queue.take();
                    System.out.println("消费: " + value);
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

第六部分:线程池(第二课时重点)

6.1 为什么需要线程池?

线程的创建和销毁是有成本的。频繁创建销毁线程会导致:

  • 系统开销大(每次创建都需要系统调用)
  • 资源管理混乱
  • 系统稳定性下降

线程池的优势

  • 复用线程,减少创建销毁开销
  • 控制并发数,避免资源耗尽
  • 统一管理,方便监控和调优

6.2 ThreadPoolExecutor核心参数

ThreadPoolExecutor是线程池的核心实现,它有7个关键参数:

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,      // 核心线程数
    int maximumPoolSize,   // 最大线程数
    long keepAliveTime,    // 非核心线程空闲存活时间
    TimeUnit unit,         // 时间单位
    BlockingQueue<Runnable> workQueue, // 任务队列
    ThreadFactory threadFactory,       // 线程工厂
    RejectedExecutionHandler handler   // 拒绝策略
)
6.2.1 参数详解
  • corePoolSize:核心线程数,即使空闲也会保留
  • maximumPoolSize:最大线程数,线程池允许创建的最大线程数量
  • keepAliveTime:非核心线程空闲存活时间,超过此时间会被回收
  • workQueue:任务队列,存放等待执行的任务
  • threadFactory:线程工厂,用于创建新线程(可自定义)
  • handler:拒绝策略,当任务无法处理时的处理方式
6.2.2 线程池工作流程
复制代码
提交任务 →
   如果当前线程数 < corePoolSize → 创建新线程执行任务
   否则 → 任务加入 workQueue
       如果队列已满 → 创建新线程(但不超过 maximumPoolSize)
           如果线程数已达 maximumPoolSize → 执行拒绝策略

6.3 拒绝策略

当任务无法被处理时,线程池有4种内置拒绝策略:

策略 行为
AbortPolicy 抛出RejectedExecutionException(默认)
CallerRunsPolicy 由提交任务的线程自己执行
DiscardPolicy 直接丢弃,不抛异常
DiscardOldestPolicy 丢弃队列中最老的任务,重新提交

6.4 线程池创建方式

6.4.1 通过Executors工厂类

Executors提供了几种快捷创建方式:

java 复制代码
// 固定线程数线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(5);

// 可缓存线程池
ExecutorService cachedPool = Executors.newCachedThreadPool();

// 单线程线程池
ExecutorService singlePool = Executors.newSingleThreadExecutor();

// 定时任务线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
6.4.2 为什么不推荐Executors?

阿里巴巴Java开发手册明确规定:线程池不允许使用Executors创建,而是通过ThreadPoolExecutor的方式

原因在于Executors创建的线程池存在资源耗尽风险:

类型 问题
FixedThreadPool 使用无界LinkedBlockingQueue,任务积压可能OOM
CachedThreadPool 最大线程数无限,创建过多线程可能OOM
SingleThreadPool 同样使用无界队列
6.4.3 正确姿势:直接使用ThreadPoolExecutor
java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,                         // corePoolSize
    10,                        // maximumPoolSize
    60, TimeUnit.SECONDS,      // keepAliveTime
    new LinkedBlockingQueue<>(100), // 有界队列,容量100
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy()
);

6.5 线程数配置策略

合理配置线程数需要根据任务类型决定:

CPU密集型任务
  • 特征:大量计算,CPU使用率高
  • 建议:线程数 = CPU核心数 + 1
  • 原因:避免过多线程导致频繁上下文切换
IO密集型任务
  • 特征:大量等待(网络、磁盘),CPU使用率低
  • 建议:线程数 = CPU核心数 * 2
  • 原因:线程在等待IO时,CPU可以调度其他线程执行
混合型任务
  • 将任务拆分为CPU密集和IO密集两部分,或通过压测确定最优值

6.6 线程池监控

通过ThreadPoolExecutor提供的方法可以监控线程池状态:

java 复制代码
int activeCount = executor.getActiveCount();      // 活跃线程数
long taskCount = executor.getTaskCount();         // 总任务数
long completedCount = executor.getCompletedTaskCount(); // 已完成任务数
int queueSize = executor.getQueue().size();       // 队列长度

6.7 优雅关闭线程池

java 复制代码
// 1. 不再接受新任务
executor.shutdown();

try {
    // 2. 等待现有任务完成(最多60秒)
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        // 3. 强制停止
        executor.shutdownNow();
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
}

第七部分:JUC优化与实战(第二课时重点)

7.1 锁优化策略

7.1.1 减少锁持有时间
java 复制代码
// 不好的做法:锁住整个方法
public synchronized void process() {
    prepare();    // 准备阶段(无需锁)
    doWork();     // 需要同步
    cleanup();    // 清理阶段(无需锁)
}

// 好的做法:只锁必要部分
public void process() {
    prepare();    // 无锁
    synchronized(this) {
        doWork(); // 需要同步
    }
    cleanup();    // 无锁
}
7.1.2 减小锁粒度
  • 使用ConcurrentHashMap代替Hashtable
  • 使用读写锁代替独占锁
  • 使用分段锁或桶锁
7.1.3 锁分离

读写锁本质就是一种锁分离。还有更进一步的分离,如LinkedBlockingQueue取锁和存锁分离

7.2 CAS优化

  • 使用LongAdder替代AtomicLong :在高并发下,LongAdder通过分段累加减少CAS冲突
  • 合理设置自旋次数:避免长时间自旋浪费CPU

7.3 并发编程最佳实践

7.3.1 优先使用并发工具而非手动同步
java 复制代码
// 不好的做法:手动同步
Map<String, String> map = new HashMap<>();
synchronized(map) {
    map.put(key, value);
}

// 好的做法:使用ConcurrentHashMap
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put(key, value);
7.3.2 使用不可变对象

不可变对象天然线程安全,无需同步。例如String、基本类型包装类等。

7.3.3 线程封闭

通过ThreadLocal将共享变量限制在单个线程内,避免同步:

java 复制代码
ThreadLocal<SimpleDateFormat> df = ThreadLocal.withInitial(
    () -> new SimpleDateFormat("yyyy-MM-dd")
);
// 每个线程有自己的SimpleDateFormat实例,无需同步
7.3.4 避免死锁

遵循固定锁顺序原则:

java 复制代码
// 按对象hashCode决定获取顺序,避免循环等待
public void transferMoney(Account from, Account to, int amount) {
    int fromHash = System.identityHashCode(from);
    int toHash = System.identityHashCode(to);
    
    if (fromHash < toHash) {
        synchronized (from) {
            synchronized (to) {
                // 转账逻辑
            }
        }
    } else if (fromHash > toHash) {
        synchronized (to) {
            synchronized (from) {
                // 转账逻辑
            }
        }
    } else {
        // 如果hashCode相等,加一个额外锁
        synchronized (lock) {
            synchronized (from) {
                synchronized (to) {
                    // 转账逻辑
                }
            }
        }
    }
}

7.4 性能对比测试

场景 建议方案 原因
简单计数器 AtomicInteger 无锁,性能最高
读多写少Map ConcurrentHashMap 读无锁,并发度高
读多写少List CopyOnWriteArrayList 读无锁,适合读远多于写
高并发写Map ConcurrentHashMap 桶锁,并发度高
生产者-消费者 BlockingQueue 内置阻塞机制,使用简单
定时任务 ScheduledThreadPoolExecutor 比Timer更可靠

7.5 常见问题排查

7.5.1 线程池队列积压
  • 原因:消费者处理速度跟不上生产者
  • 对策:增加消费者线程数,或优化处理逻辑
7.5.2 死锁
  • 现象:程序卡死,jstack显示BLOCKED状态
  • 排查:使用jstack查看锁持有和等待关系
  • 解决:确保锁获取顺序一致,使用tryLock带超时
7.5.3 内存泄漏
  • 现象:ConcurrentHashMap无限增长
  • 原因:没有移除机制,或ThreadLocal使用后未remove
  • 解决:确保有移除策略,ThreadLocal用完后调用remove()

课程总结

通过两个课时的系统学习,我们全面覆盖了JUC并发编程的五大核心组件:

  1. locks包ReentrantLock、读写锁,基于AQS的灵活锁机制
  2. atomic包:原子类与CAS原理,无锁编程的基石
  3. tools包CountDownLatchCyclicBarrierSemaphore等并发工具
  4. collections包ConcurrentHashMapCopyOnWriteArrayList等线程安全容器
  5. executor包:线程池框架,合理的资源管理

核心原理回顾

  • AQS:JUC的基石,通过state和CLH队列实现同步器
  • CAS:原子操作的基础,但需注意ABA问题
  • 锁升级:从偏向锁到轻量级锁再到重量级锁的动态演进
  • 分段锁/桶锁:ConcurrentHashMap的并发优化思路

实战要点

  • 线程池参数配置:根据任务类型(CPU密集/IO密集)合理设置
  • 锁优化:减少持有时间、减小粒度、读写分离
  • 工具选择:根据场景选择最合适的并发工具
  • 异常处理 :妥善处理InterruptedException,保留线程中断状态
相关推荐
清水白石0082 小时前
突破性能瓶颈:深度解析 Numba 如何让 Python 飙到 C 语言的速度
开发语言·python
Eternity∞2 小时前
Linux系统下,C语言基础
linux·c语言·开发语言
长路 ㅤ   2 小时前
Java AWT剪贴板操作踩坑记:HeadlessException异常分析与解决方案
spring boot·java剪贴板·eventqueue·awt线程调度
前路不黑暗@3 小时前
Java项目:Java脚手架项目的登录认证服务(十三)
java·spring boot·笔记·学习·spring·spring cloud·maven
wangluoqi3 小时前
c++ 树上问题 小总结
开发语言·c++
番茄去哪了3 小时前
苍穹外卖day05----店铺营业状态设置
java·数据库·ide·redis·git·maven·mybatis
Go_Zezhou4 小时前
pnpm下载后无法识别的问题及解决方法
开发语言·node.js
QQ 31316378904 小时前
文华指标公式大全通道划线指标
java
前路不黑暗@4 小时前
Java项目:Java脚手架项目的 C 端用户服务(十五)
java·开发语言·spring boot·学习·spring cloud·maven·mybatis