JUC 并发容器与工具

4 阅读7分钟

JUC — — java.util.concurrent

java的并发容器与工具,接下来列出一些工作中高频使用的,不列冷门类。

一、并发Collection

ConcurrentHashMap

数组 + 链表 + 红黑树

可以看出,底层数据结构和HashMap是一致的。

初始容量16,每个数据发生数据冲突后,挂在数据后面,形成链表。

挂的多了,链表长度 ≥ 8,且数组长度 ≥ 64时,链表转换为红黑树,增加查询效率 O(log n)

两者数据结构基本一样,但是并发安全的处理上天差地别。(以jdk 8+为准)

// HashMap:整个数组加锁
synchronized (map) {
    map.put(key, value);  // 其他线程都不能put和get
}

// ConcurrentHashMap:只锁当前桶
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 桶为空 → CAS 无锁插入
    if (tabAt(tab, i) == null) {
        casTabAt(tab, i, null, newNode);  // 无锁!
    } else {
        // 桶非空 → 锁住桶的头节点
        synchronized (f) {
            // 在链表或红黑树中插入
        }
    }
}
操作HashMapConcurrentHashMap
写操作(put)全表锁(整个数组)锁单个桶(链表头或红黑树根节点)
读操作(get)无锁(但线程不安全)无锁(volatile 保证可见性)

HashMap是一间只有一个门的房间,一次只能进一个人;

ConcurrentHashMap是一间有无数个隔间的仓库,每个人可以进自己的隔间,互不打扰。

使用案例:本地缓存

场景:缓存从数据库查询的商品信息,避免频繁查库。

public class ProductCache {
    // 商品ID -> 商品信息的映射
    private final ConcurrentHashMap<Long, Product> cache = new ConcurrentHashMap<>();

    public Product getProduct(Long productId) {
        // 1. 先查缓存
        Product product = cache.get(productId);
        if (product != null) {
            return product;
        }

        // 2. 缓存未命中,查数据库
        product = queryFromDB(productId);
        if (product != null) {
            cache.put(productId, product);  // 放入缓存
        }
        return product;
    }

    // 商品信息变更时,更新缓存
    public void updateProduct(Product product) {
        cache.put(product.getId(), product);
    }

    // 商品下架时,移除缓存
    public void removeProduct(Long productId) {
        cache.remove(productId);
    }

    private Product queryFromDB(Long productId) {
        // 模拟数据库查询
        return new Product(productId, "商品" + productId, 100.0);
    }
}

CopyOnWriteArrayList

写时复制,读无锁,写加锁全量复制,适用于CopyOnWriteArrayList的情况。

public class CopyOnWriteArrayList<E> implements ... {
    /** 底层存储,永远是 volatile 数组 */
    private transient volatile Object[] array;

    /** 独占锁,保护写操作 */
    final transient ReentrantLock lock = new ReentrantLock();
}
public boolean add(E e) {
    final ReentrantLock lock = this.lock;

    // ★ 1. 加锁 —— 同一时间只允许一个线程写
    lock.lock();
    try {
        Object[] elements = getArray();     // 拿到旧数组
        int len = elements.length;

        // ★ 2. 写时复制 —— 拷贝一份新数组(长度+1)
        Object[] newElements = Arrays.copyOf(elements, len + 1);

        // ★ 3. 在新数组上修改
        newElements[len] = e;

        // ★ 4. 原子替换 —— volatile write,读线程立即可见
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();  // 释放锁
    }
}

使用案例:黑白名单过滤器

场景:网关服务需要维护一个动态的黑名单IP列表,管理员可以随时增删,但每次请求都要检查。

@Component
public class IpBlacklistFilter {
    // 黑名单列表,读多写极少
    private final CopyOnWriteArrayList<String> blacklist = new CopyOnWriteArrayList<>();

    @PostConstruct
    public void init() {
        // 启动时加载初始黑名单
        blacklist.addAll(Arrays.asList("192.168.1.100", "10.0.0.50"));
    }

    /**
     * 每次请求都会调用,读操作无锁,性能极高
     */
    public boolean isBlocked(String ip) {
        return blacklist.contains(ip);  // 读操作,不加锁
    }

    /**
     * 管理员手动添加黑名单,写操作较少
     */
    public void addToBlacklist(String ip) {
        blacklist.addIfAbsent(ip);  // 不存在才添加
        System.out.println("黑名单添加: " + ip);
    }

    /**
     * 管理员手动移除黑名单
     */
    public void removeFromBlacklist(String ip) {
        blacklist.remove(ip);
        System.out.println("黑名单移除: " + ip);
    }

    /**
     * 获取当前黑名单列表(遍历快照)
     */
    public List<String> getBlacklist() {
        return new ArrayList<>(blacklist);  // 返回快照,避免外部修改
    }
}

// 使用
@RestController
public class GatewayController {
    @Autowired
    private IpBlacklistFilter blacklistFilter;

    @GetMapping("/api/**")
    public ResponseEntity<?> handleRequest(HttpServletRequest request) {
        String clientIp = request.getRemoteAddr();
        if (blacklistFilter.isBlocked(clientIp)) {
            return ResponseEntity.status(403).body("Forbidden");
        }
        // 正常处理请求
        return ResponseEntity.ok("Success");
    }
}

二、并发工具

CountDownLatch

CountDownLatch 是 Java 并发包中的一个倒计数器同步工具

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

核心概念:

  • 计数器:初始值设置一个正数的计数
  • await():调用此方法会阻塞等待,直到计数器变成0
  • countDown():计数器不断减1

工作原理图示:

初始化: CountDownLatch latch = new CountDownLatch(3)

主线程调用 latch.await() → 阻塞等待

线程1 完成任务 → latch.countDown()  // 计数器: 3 → 2
线程2 完成任务 → latch.countDown()  // 计数器: 2 → 1
线程3 完成任务 → latch.countDown()  // 计数器: 1 → 0

计数器归零 → 主线程被唤醒 → 继续执行后续代码

案例:线程池创建任务,使用计数器保证所有线程完成后继续执行后续代码

public class SimpleCountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        int taskCount = 5;
        CountDownLatch latch = new CountDownLatch(taskCount);
        ExecutorService executor = Executors.newFixedThreadPool(10);

        for (int i = 0; i < taskCount; i++) {
            int taskId = i;
            executor.execute(() -> {
                try {
                    System.out.println("任务" + taskId + " 执行中...");
                    Thread.sleep(1000);
                    System.out.println("任务" + taskId + " 完成");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown(); // 每个任务完成时计数器减1
                }
            });
        }

        System.out.println("主线程等待所有任务完成...");
        latch.await(); // 阻塞,直到计数器归零
        System.out.println("所有任务完成,主线程继续执行");

        executor.shutdown();
    }
}

Semaphore

Semaphore 本质上就是基于 AQS(AbstractQueuedSynchronizer) 的一个共享锁实现。

Semaphore(3)
     │
     ▼
AQS.state = 3   ← 许可证数量保存在 state 中
     │
     ├── acquire() → CAS 尝试将 state 减 1
     │                  ├── 成功(state >= 0)→ 获得许可,继续执行
     │                  └── 失败(state < 0) → 进入 CLH 队列阻塞等待
     │
     └── release() → CAS 将 state 加 1
                      └── 唤醒队列中等待的第一个线程

源码简化:

// acquire() 的底层逻辑
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);  // 调用 AQS 的共享模式获取
}

// AQS 内部
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (tryAcquireShared(arg) < 0) {     // 尝试获取,返回负数表示失败
        doAcquireSharedInterruptibly(arg); // 失败则入队阻塞
    }
}

// Semaphore 的非公平实现
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
    int available = getState();       // 当前可用许可数
    int remaining = available - acquires;
    if (remaining < 0 ||              // 许可不够
        compareAndSetState(available, remaining)) { // CAS 更新
        return remaining;
    }
}
}

// release() 的底层逻辑
protected final boolean tryReleaseShared(int releases) {
for (;;) {
    int current = getState();
    int next = current + releases;
    if (compareAndSetState(current, next)) { // CAS 增加许可
        return true;
    }
}
}

本质就是用cas保证并发的情况下,使用aqs的阻塞队列等待,state是volatile,保证线程可见性的

当state < 0时,不许可,当state > 0时,可以执行。

限流这种需求很重要,是控制并发请求数的,以下为Semaphore的案例

public class ApiRateLimiter {
    private final Semaphore semaphore;

    public ApiRateLimiter(int maxConcurrentRequests) {
        this.semaphore = new Semaphore(maxConcurrentRequests);
    }

    public void handleRequest(String requestId) {
        // 尝试获取许可,获取不到直接返回失败
        if (!semaphore.tryAcquire()) {
            System.out.println("请求 " + requestId + " 被限流,返回 503");
            return;
        }

        try {
            System.out.println("请求 " + requestId + " 开始处理,当前并发:" 
                    + (maxConcurrent - semaphore.availablePermits()));
            // 实际的业务处理
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            semaphore.release();
            System.out.println("请求 " + requestId + " 处理完成");
        }
    }

    public static void main(String[] args) {
        ApiRateLimiter limiter = new ApiRateLimiter(3); // 最多3个并发
        
        // 模拟10个并发请求
        for (int i = 0; i < 10; i++) {
            int id = i;
            new Thread(() -> limiter.handleRequest("req-" + id)).start();
        }
    }
}

案例2:数据库连接池(更真实的限流)

public class DatabasePool {
    private final Semaphore semaphore;
    private final int maxConnections;

    public DatabasePool(int maxConnections) {
        this.maxConnections = maxConnections;
        this.semaphore = new Semaphore(maxConnections, true); // 公平模式
    }

    public void executeQuery(String sql) {
        boolean acquired = false;
        try {
            // 尝试获取连接,最多等2秒
            acquired = semaphore.tryAcquire(2, TimeUnit.SECONDS);
            if (!acquired) {
                System.out.println("获取连接超时,请稍后重试");
                return;
            }

            // 模拟执行SQL
            System.out.println(Thread.currentThread().getName() 
                    + " 执行: " + sql 
                    + ",活跃连接数: " + (maxConnections - semaphore.availablePermits()));
            Thread.sleep(1000);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (acquired) {
                semaphore.release();
            }
        }
    }

    public static void main(String[] args) {
        DatabasePool pool = new DatabasePool(3); // 最多3个数据库连接
        
        // 模拟20个查询请求
        for (int i = 0; i < 20; i++) {
            int queryId = i;
            new Thread(() -> pool.executeQuery("SELECT * FROM orders WHERE id=" + queryId)).start();
        }
    }
}

案例3:文件上传限流(控制磁盘IO并发)

public class FileUploadLimiter {
    private final Semaphore semaphore;

    public FileUploadLimiter(int maxConcurrentUploads) {
        this.semaphore = new Semaphore(maxConcurrentUploads);
    }

    public void uploadFile(String fileName, byte[] data) {
        try {
            semaphore.acquire();
            System.out.println("开始上传: " + fileName);
            
            // 模拟文件写入
            try (FileOutputStream fos = new FileOutputStream(new File(fileName))) {
                fos.write(data);
                Thread.sleep(2000); // 模拟上传耗时
            }
            
            System.out.println("上传完成: " + fileName);
        } catch (Exception e) {
            System.out.println("上传失败: " + fileName);
        } finally {
            semaphore.release();
        }
    }

    public static void main(String[] args) {
        FileUploadLimiter limiter = new FileUploadLimiter(2); // 最多同时上传2个文件
        
        for (int i = 0; i < 10; i++) {
            int fileId = i;
            new Thread(() -> limiter.uploadFile("file_" + fileId + ".txt", 
                    "data".getBytes())).start();
        }
    }
}

到目前为止我们学的所有并发知识——锁、CAS、AQS、并发容器——解决的都是线程之间抢资源的问题。

这些问题的特点是:冲突发生在 CPU 内部,速度极快

  • 一个 CAS 操作:几十纳秒
  • 一个锁的获取和释放:几微秒
  • 一个线程上下文切换:几微秒

这些开销虽然存在,但对于业务系统来说,基本可以接受。你写十个线程抢一个 AtomicInteger,性能损失微乎其微。

当线程遇到 I/O 时,画风突变。来看一组数据

操作耗时比例
CPU 执行一条指令0.3 纳秒1x
一次内存访问100 纳秒300x
一次 SSD 随机读10 微秒30,000x
一次网络请求(同机房)0.5 毫秒1,500,000x
一次数据库查询10 毫秒30,000,000x
一次磁盘寻道10 毫秒30,000,000x

一个数据库查询的时间,足够 CPU 执行 3000 万条指令。

这意味着什么?意味着如果你的线程在等数据库返回结果,它基本上就是在睡觉——而且是深度睡眠。

下一站:BIO、NIO、AIO,当线程遇上 I/O,才是并发真正的战场。