JUC 并发容器与工具

JUC --- --- java.util.concurrent

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

一、并发Collection

ConcurrentHashMap

数组 + 链表 + 红黑树

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

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

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

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

java 复制代码
// 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) {
            // 在链表或红黑树中插入
        }
    }
}
操作 HashMap ConcurrentHashMap
写操作(put) 全表锁(整个数组) 锁单个桶(链表头或红黑树根节点)
读操作(get) 无锁(但线程不安全) 无锁(volatile 保证可见性)

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

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

使用案例:本地缓存

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

typescript 复制代码
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的情况。

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

    /** 独占锁,保护写操作 */
    final transient ReentrantLock lock = new ReentrantLock();
}
ini 复制代码
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列表,管理员可以随时增删,但每次请求都要检查。

typescript 复制代码
@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

工作原理图示:

arduino 复制代码
初始化: CountDownLatch latch = new CountDownLatch(3)

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

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

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

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

csharp 复制代码
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) 的一个共享锁实现。

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

源码简化:

arduino 复制代码
// 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的案例

csharp 复制代码
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:数据库连接池(更真实的限流)

java 复制代码
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并发)

csharp 复制代码
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,才是并发真正的战场。

相关推荐
威武的花瓣2 小时前
细说ASP.NET的各种异步操作
后端·asp.net·php
漂亮的摩托2 小时前
如何编写一个SpringBoot项目告警推送的Starter
java·spring boot·后端
任性的芝麻2 小时前
ASP.NET MVC 中的异步方式
后端·asp.net·mvc
雨师@2 小时前
go语言项目--实例化(图书管理)--006
开发语言·后端·golang
kuro-shiro2 小时前
SpringBoot 启动流程
java·spring boot·后端
独孤九剑打醒他11 小时前
双层Master-Worker软硬协同调度架构:从根源解决分布式数据一致性难题
后端·嵌入式硬件·硬件架构·硬件工程
不会c+13 小时前
02-SpringBoot配置文件
java·spring boot·后端
雨辰AI14 小时前
生产级实战:人大金仓 V9 标准化运维手册(日常巡检 + 监控告警 + 应急处置)
java·运维·数据库·后端
TeamDev14 小时前
JxBrowser 9.3.0 版本发布啦!
java·后端·c#·混合应用·jxbrowser·浏览器控件·异步媒体设备