并发编程(七)

一、前言

在Java并发编程中,我们前面学习了Java内存模型、volatile、synchronized以及锁的底层原理(AQS、CAS)。这些知识让我们能够自己编写线程安全的代码。但在实际开发中,我们往往不需要"重新发明轮子"。JDK已经提供了大量现成的并发容器和并发工具类,它们在内部已经实现了必要的同步机制,我们只需直接调用即可。

本章将深入剖析ConcurrentHashMap、阻塞队列、Fork/Join框架、原子类以及CountDownLatch、CyclicBarrier等并发工具的原理与使用。掌握这些,你的并发编程能力将再上一个台阶。

二、并发容器框架:告别手写锁

并发容器框架指的是java.util.concurrent包下的一系列数据结构和工具类。它们的特点是:

内部已经实现了线程安全机制(如加锁、CAS等)。

我们多线程调用它们的方法时,无需额外编写同步代码。

性能经过高度优化,比我们自己用synchronized包装普通容器要好得多。

一句话总结:拿来即用,线程安全,高效可靠。

三、ConcurrentHashMap:线程安全的HashMap

3.1 HashMap的线程不安全问题

HashMap是Java中最常用的键值对集合,它在单线程下性能优秀,但在多线程下却会引发严重问题:

数据覆盖:多个线程同时put,后写入的值可能覆盖前一个,导致数据丢失。

死循环(JDK 1.7及以前):多线程并发扩容时,链表可能形成环形结构,导致CPU 100%并最终宕机。死循环的根本原因是扩容时对链表进行rehash,头插法在多线程下造成节点互相引用。

因此,在多线程环境中绝对不能直接使用HashMap。

3.2 Hashtable:古老但低效

Hashtable是线程安全的,它通过对整个哈希表加锁(synchronized方法)来保证安全。但这也意味着同一时刻只有一个线程能够访问哈希表,并发性能极差。

3.3 ConcurrentHashMap:分段锁与高效并发

ConcurrentHashMap在JDK 1.7中采用分段锁(Segment)机制:内部维护一个Segment数组,每个Segment相当于一个小型的HashMap,并且自己持有一把锁。不同Segment的读写可以并发执行,从而大幅提高并发度。

JDK 1.8后,ConcurrentHashMap放弃了分段锁,改用CAS + synchronized对每个桶(Node)的头节点进行细粒度锁,进一步提升了并发性能。

面试重点:HashMap、Hashtable、ConcurrentHashMap三者的区别,以及ConcurrentHashMap的底层实现。

示例代码:使用ConcurrentHashMap

复制代码
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

public class ConcurrentHashMapDemo {
    public static void main(String[] args) {
        Map<String, Integer> map = new ConcurrentHashMap<>();

        // 多线程环境下直接put和get,无需额外同步
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                String key = Thread.currentThread().getName() + "-" + i;
                map.put(key, i);
                map.get(key);
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Map size: " + map.size());
    }
}

四、阻塞队列(BlockingQueue)

阻塞队列是一种支持阻塞插入和阻塞移除的队列。当队列满时,插入线程会被阻塞直到队列有空间;当队列空时,移除线程会被阻塞直到队列有元素。

4.1 常用阻塞队列实现

ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO。

LinkedBlockingQueue:基于链表的可选有界阻塞队列,吞吐量通常高于ArrayBlockingQueue。

PriorityBlockingQueue:支持优先级排序的无界阻塞队列。

SynchronousQueue:不存储元素的阻塞队列,每个插入必须等待一个移除,反之亦然。

LinkedTransferQueue:基于链表的无界阻塞队列,支持transfer操作。

LinkedBlockingDeque:双向阻塞队列。

4.2 有界队列 vs 无界队列

有界队列:设置容量上限,当队列满时触发拒绝策略或阻塞。在生产者-消费者模型中,有界队列可以防止生产者过快导致内存溢出,并且能配合线程池的最大线程数触发扩容。

无界队列:理论容量无限(受内存限制)。会导致任务无限堆积,最终OOM,且无法触发线程池的最大线程数。

实际生产中,更推荐使用有界队列(如ArrayBlockingQueue、LinkedBlockingQueue指定容量)。

示例:使用ArrayBlockingQueue实现生产者-消费者

复制代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class BlockingQueueDemo {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

        // 生产者
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 20; i++) {
                    queue.put(i);
                    System.out.println("生产: " + i);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 消费者
        Thread consumer = new Thread(() -> {
            try {
                while (true) {
                    Integer value = queue.take();
                    System.out.println("消费: " + value);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();
    }
}

五、Fork/Join框架:分治并行

Fork/Join框架是JDK 7引入的用于并行执行任务的框架,核心思想是"分而治之"。一个大任务拆分成多个小任务(fork),小任务分别执行,最后将结果合并(join)。

适用场景:递归分解型任务,如归并排序、斐波那契数列、数组求和等。

核心类:RecursiveTask(有返回值)和RecursiveAction(无返回值)。

示例:使用Fork/Join计算1到1000000的和

复制代码
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class ForkJoinSumTask extends RecursiveTask<Long> {
    private static final int THRESHOLD = 10000;
    private final int start;
    private final int end;

    public ForkJoinSumTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if (end - start <= THRESHOLD) {
            // 小任务直接计算
            long sum = 0;
            for (int i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
            // 拆分任务
            int mid = (start + end) >>> 1;
            ForkJoinSumTask left = new ForkJoinSumTask(start, mid);
            ForkJoinSumTask right = new ForkJoinSumTask(mid + 1, end);
            left.fork();  // 异步执行左任务
            right.fork(); // 异步执行右任务
            return left.join() + right.join(); // 等待结果并合并
        }
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        ForkJoinSumTask task = new ForkJoinSumTask(1, 1_000_000);
        long result = pool.invoke(task);
        System.out.println("结果: " + result);
    }
}

注意:ForkJoinPool采用了工作窃取算法,空闲线程可以"窃取"其他任务队列中的任务,从而提高CPU利用率。

六、原子类(Atomic)

原子类位于java.util.concurrent.atomic包下,它们利用CAS(Compare-And-Swap)实现了轻量级的线程安全操作,适用于低并发场景下的计数器、状态标识等。

6.1 常用原子类

AtomicInteger / AtomicLong / AtomicBoolean

AtomicIntegerArray / AtomicLongArray / AtomicReferenceArray

AtomicReference / AtomicStampedReference(解决ABA问题)

LongAdder / LongAccumulator(高并发下性能优于AtomicLong)

6.2 为什么原子类不适合高并发?

原子类底层使用自旋CAS,在高并发(大量线程同时竞争)时,CAS失败率很高,线程会不断重试,消耗大量CPU资源。此时应使用LongAdder(内部采用分段累加,最后再汇总)或改用锁。

示例:AtomicInteger的线程安全自增

复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[100];
        for (int i = 0; i < 100; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    count.incrementAndGet(); // 原子自增
                }
            });
            threads[i].start();
        }
        for (Thread t : threads) {
            t.join();
        }
        System.out.println("最终结果: " + count.get()); // 预期 100000
    }
}

6.3 原子类与volatile的区别

volatile只能保证可见性和有序性,不能保证复合操作的原子性(如i++)。

原子类通过CAS保证了复合操作的原子性。

七、并发工具类:CountDownLatch、CyclicBarrier

7.1 CountDownLatch:等待所有子任务完成

CountDownLatch允许一个或多个线程等待其他线程执行完毕。它内部维护一个计数器,每次调用countDown()减一,await()会阻塞直到计数器为0。

典型场景:主线程等待多个子线程完成初始化后再开始工作。

示例:主线程等待三个子线程全部启动后再继续

复制代码
import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);

        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行任务");
                latch.countDown(); // 完成一个,计数器减一
            }).start();
        }

        latch.await(); // 主线程等待计数器为0
        System.out.println("所有子任务完成,主线程继续");
    }
}

7.2 CyclicBarrier:等待所有线程到达屏障点

CyclicBarrier让一组线程互相等待,直到所有线程都到达某个公共屏障点,然后所有线程才被唤醒继续执行。与CountDownLatch不同,CyclicBarrier可以重复使用(reset())。

示例:模拟玩家加载进度,所有玩家加载完成后再开始游戏

复制代码
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(4, () -> {
            System.out.println("所有玩家已加载完成,游戏开始!");
        });

        for (int i = 0; i < 4; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " 正在加载...");
                try {
                    Thread.sleep((long) (Math.random() * 3000));
                    barrier.await(); // 等待其他线程
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

八、总结

ConcurrentHashMap是HashMap的线程安全替代品,分段锁和CAS保证了高并发下的性能。

阻塞队列是实现生产者消费者模式的核心组件,有界队列更可靠。

Fork/Join框架适合任务可分治的并行计算,工作窃取算法提高效率。

原子类利用CAS实现轻量级线程安全,低并发时性能优秀,高并发请用LongAdder或锁。

CountDownLatch用于等待一组任务完成,CyclicBarrier用于多线程互相等待到达屏障点。

这些并发容器和工具类极大简化了Java并发编程的复杂度。理解它们的原理和适用场景,是成为高级Java工程师的必经之路。在实际项目中,优先使用这些成熟组件,而不是重复编写低效且易错的同步代码。

相关推荐
亦暖筑序2 小时前
单模型成本高、风险大?Spring AI多模型路由实战:成本降70%,可用性更稳
java·后端·ai编程
404号扳手2 小时前
Java 进阶知识(二)
java·后端
SamDeepThinking2 小时前
一个业务场景只需要一个ThreadLocal实例
java·后端·程序员
带刺的坐椅2 小时前
Solon 热加载与插件热插拔:Debug 模式 × E-Spi × H-Spi 全解析
java·solon·插件·plugin·热插拨
Rick19932 小时前
mysql联合索引经典实例
java·数据库·mysql
方也_arkling2 小时前
【Java-Day02】语法篇:变量/数据类型/标识符/运算符/类型转换
java·开发语言
学代码的真由酱3 小时前
WebSocket背景知识及简单实现-Java
java·websocket
lld9510273 小时前
(一)云回测:量化策略上线前的必经之路
java·服务器·数据库
云云只是个程序马喽3 小时前
海外短剧系统开发_云微传媒:多语言短剧平台定制与变现解决方案
java·php