JAVA多线程并发编程:并发容器与线程协作实战

JAVA多线程并发编程:并发容器与线程协作实战

JAVA多线程并发编程:并发容器与线程协作实战

在这里插入图片描述

💡 学习目标:掌握JAVA中常用并发容器的特性与适用场景,理解线程间协作的核心原理,能够运用并发容器和协作工具解决实际并发问题。
💡 学习重点:并发容器与普通容器的区别、ConcurrentHashMap 核心原理、CountDownLatch/CyclicBarrier/Semaphore 的使用、生产者消费者模式实现。

1.1 为什么需要并发容器?

在多线程场景下,普通的集合容器(如 HashMapArrayList)是线程不安全的。多个线程同时对其进行读写操作时,会导致数据错乱、ConcurrentModificationException 异常等问题。

⚠️ 注意事项:即使使用 Collections.synchronizedXXX() 方法包装普通容器,也只是通过 synchronized 实现简单的加锁。这种方式锁粒度较粗,并发性能较低。

核心结论:并发容器是JAVA为多线程场景设计的高性能容器。它们通过细粒度锁无锁算法实现线程安全,能够在保证数据一致性的同时,大幅提升并发访问效率。

1.2 常用并发容器详解

1.2.1 ConcurrentHashMap:高效并发哈希表

ConcurrentHashMapHashMap 的并发安全版本,是日常开发中使用频率最高的并发容器。

1.2.1.1 核心特点
  1. 分段锁(JDK1.7)→ CAS + 同步锁(JDK1.8)
    • JDK1.7采用分段锁机制,将数据分成多个Segment。每个Segment独立加锁,不同Segment的操作互不阻塞。
    • JDK1.8抛弃分段锁,采用 CAS + synchronized 实现。锁粒度缩小到单个Node节点,并发性能进一步提升。
  2. 支持高并发读写:读操作无锁(通过 volatile 保证可见性),写操作仅锁定当前节点,不会阻塞其他操作。
  3. 不允许 null 键值:与 HashMap 不同,ConcurrentHashMap 的键和值都不能为 null,避免歧义。
1.2.1.2 核心方法使用示例
importjava.util.concurrent.ConcurrentHashMap;/** * ConcurrentHashMap 实战示例 */publicclassConcurrentHashMapDemo{publicstaticvoidmain(String[] args)throwsInterruptedException{ConcurrentHashMap<String,Integer> map =newConcurrentHashMap<>();// 1. 插入数据 map.put("apple",10); map.put("banana",20); map.put("orange",30);// 2. 并发修改数据Thread thread1 =newThread(()->{for(int i =0; i <5; i++){// 原子性操作:获取并增加 map.computeIfPresent("apple",(k, v)-> v +1);System.out.println(Thread.currentThread().getName()+" : apple = "+ map.get("apple"));}},"线程1");Thread thread2 =newThread(()->{for(int i =0; i <5; i++){ map.computeIfPresent("apple",(k, v)-> v +1);System.out.println(Thread.currentThread().getName()+" : apple = "+ map.get("apple"));}},"线程2"); thread1.start(); thread2.start(); thread1.join(); thread2.join();// 3. 遍历数据 map.forEach((k, v)->System.out.println(k +" : "+ v));}}
1.2.1.3 适用场景
  • 高并发下的键值对存储场景,如缓存、用户会话存储。
  • 读多写少的业务场景,能充分发挥其无锁读的性能优势。

1.2.2 CopyOnWriteArrayList:写时复制数组

CopyOnWriteArrayListArrayList 的并发安全版本,核心思想是写时复制

1.2.2.1 核心原理

① 📝 写操作:当执行添加、删除、修改操作时,会复制一份新的数组。在新数组上完成操作后,再将原数组的引用指向新数组。
② 🔍 读操作:直接读取原数组,无需加锁,保证了读操作的高效性。
③ ⚠️ 数据一致性:写操作是原子性的,读操作可能读取到旧数据。该容器适用于读多写少的场景。

1.2.2.2 使用示例
importjava.util.Iterator;importjava.util.concurrent.CopyOnWriteArrayList;/** * CopyOnWriteArrayList 实战示例 */publicclassCopyOnWriteArrayListDemo{publicstaticvoidmain(String[] args)throwsInterruptedException{CopyOnWriteArrayList<String> list =newCopyOnWriteArrayList<>(); list.add("Java"); list.add("Python"); list.add("Go");// 1. 并发遍历与添加Thread writeThread =newThread(()->{ list.add("C++");System.out.println(Thread.currentThread().getName()+" 添加元素:C++");},"写线程");Thread readThread =newThread(()->{Iterator<String> iterator = list.iterator();while(iterator.hasNext()){System.out.println(Thread.currentThread().getName()+" 遍历元素:"+ iterator.next());// 模拟延迟try{Thread.sleep(100);}catch(InterruptedException e){ e.printStackTrace();}}},"读线程"); readThread.start();Thread.sleep(50); writeThread.start(); readThread.join(); writeThread.join();// 2. 最终遍历结果System.out.println("最终集合元素:"+ list);}}
1.2.2.3 适用场景
  • 读操作远多于写操作的场景,如系统配置读取、日志记录列表。
  • 不要求数据实时一致性的场景,允许读取到旧数据。

1.2.3 BlockingQueue:阻塞队列

BlockingQueue 是一个支持阻塞操作的队列,是实现生产者消费者模式的核心工具。

1.2.3.1 核心特性
  • 入队阻塞:当队列已满时,入队操作会阻塞,直到队列有空闲空间。
  • 出队阻塞:当队列为空时,出队操作会阻塞,直到队列中有元素。
  • 常用实现类ArrayBlockingQueue(数组实现,有界)、LinkedBlockingQueue(链表实现,可选有界)、SynchronousQueue(同步队列,无容量)。
1.2.3.2 核心方法对比
方法类型抛出异常返回特殊值阻塞超时退出
入队add(e)offer(e)put(e)offer(e, time, unit)
出队remove()poll()take()poll(time, unit)
检查element()peek()--
1.2.3.3 使用示例
importjava.util.concurrent.ArrayBlockingQueue;importjava.util.concurrent.BlockingQueue;/** * BlockingQueue 实战示例 */publicclassBlockingQueueDemo{// 定义有界阻塞队列,容量为3privatestaticfinalBlockingQueue<String> queue =newArrayBlockingQueue<>(3);publicstaticvoidmain(String[] args){// 生产者线程newThread(()->{String[] products ={"产品A","产品B","产品C","产品D"};for(String product : products){try{System.out.println(Thread.currentThread().getName()+" 生产:"+ product); queue.put(product);Thread.sleep(500);}catch(InterruptedException e){ e.printStackTrace();}}},"生产者").start();// 消费者线程newThread(()->{while(true){try{String product = queue.take();System.out.println(Thread.currentThread().getName()+" 消费:"+ product);Thread.sleep(1000);}catch(InterruptedException e){ e.printStackTrace();}}},"消费者").start();}}

1.3 线程协作工具类

在复杂的并发场景中,需要多个线程协同完成任务。JAVA提供了 CountDownLatchCyclicBarrierSemaphore 等工具类,简化线程协作的开发。

1.3.1 CountDownLatch:倒计时门闩

CountDownLatch 允许一个或多个线程等待其他线程完成操作后,再继续执行。

1.3.1.1 核心原理
  1. 初始化时指定计数器值,该值代表需要等待的线程数量。
  2. 每个线程完成任务后,调用 countDown() 方法,计数器值减1。
  3. 主线程调用 await() 方法,会阻塞直到计数器值变为0。
  4. 计数器值不可重置CountDownLatch 只能使用一次。
1.3.1.2 实战案例:多线程任务汇总
importjava.util.concurrent.CountDownLatch;/** * CountDownLatch 实战:多线程数据统计 */publicclassCountDownLatchDemo{// 定义计数器,需要等待3个任务完成privatestaticfinalCountDownLatch latch =newCountDownLatch(3);publicstaticvoidmain(String[] args)throwsInterruptedException{System.out.println("主线程:开始执行数据统计任务");// 任务1:用户数据统计newThread(()->{try{Thread.sleep(1000);System.out.println(Thread.currentThread().getName()+":用户数据统计完成");}catch(InterruptedException e){ e.printStackTrace();}finally{ latch.countDown();}},"任务线程1").start();// 任务2:订单数据统计newThread(()->{try{Thread.sleep(1500);System.out.println(Thread.currentThread().getName()+":订单数据统计完成");}catch(InterruptedException e){ e.printStackTrace();}finally{ latch.countDown();}},"任务线程2").start();// 任务3:商品数据统计newThread(()->{try{Thread.sleep(2000);System.out.println(Thread.currentThread().getName()+":商品数据统计完成");}catch(InterruptedException e){ e.printStackTrace();}finally{ latch.countDown();}},"任务线程3").start();// 等待所有任务完成 latch.await();System.out.println("主线程:所有数据统计完成,生成汇总报表");}}
1.3.1.3 适用场景
  • 主线程等待多个子线程完成初始化任务,如系统启动时加载配置、连接资源。
  • 批量任务执行场景,需要等待所有任务完成后进行结果汇总。

1.3.2 CyclicBarrier:循环栅栏

CyclicBarrier 允许一组线程相互等待,直到所有线程都到达某个屏障点后,再继续执行。

1.3.2.1 核心原理
  1. 初始化时指定参与线程数量屏障动作。屏障动作是所有线程到达后执行的任务。
  2. 每个线程到达屏障点时,调用 await() 方法,会阻塞直到所有线程都到达。
  3. 所有线程到达后,执行屏障动作,然后重置计数器。CyclicBarrier可以重复使用
1.3.2.2 实战案例:多线程数据分片处理
importjava.util.concurrent.CyclicBarrier;/** * CyclicBarrier 实战:数据分片处理 */publicclassCyclicBarrierDemo{// 定义循环栅栏,4个线程参与,所有线程到达后执行屏障动作privatestaticfinalCyclicBarrier barrier =newCyclicBarrier(4,()->{System.out.println("屏障动作:所有分片数据处理完成,开始合并结果");});publicstaticvoidmain(String[] args){// 模拟4个数据分片for(int i =0; i <4; i++){int shard = i +1;newThread(()->{try{System.out.println(Thread.currentThread().getName()+":处理分片"+ shard +"数据");Thread.sleep(1000);System.out.println(Thread.currentThread().getName()+":分片"+ shard +"处理完成,等待其他线程");// 到达屏障点 barrier.await();System.out.println(Thread.currentThread().getName()+":结果合并完成,继续后续任务");}catch(Exception e){ e.printStackTrace();}},"分片线程"+ shard).start();}}}
1.3.2.3 CountDownLatch vs CyclicBarrier
特性CountDownLatchCyclicBarrier
计数器重置不可重置,只能用一次可重置,可重复使用
等待方向一个或多个线程等待其他线程多个线程相互等待
核心场景主线程等待子线程完成线程组协同完成任务

1.3.3 Semaphore:信号量

Semaphore 用于控制同时访问特定资源的线程数量,通过许可证机制实现资源限流。

1.3.3.1 核心原理
  1. 初始化时指定许可证数量,代表允许同时访问资源的线程数。
  2. 线程访问资源前,调用 acquire() 方法获取许可证。无可用许可证时,线程会阻塞。
  3. 线程释放资源后,调用 release() 方法归还许可证。
  4. 许可证数量可以动态调整,支持公平/非公平模式。
1.3.3.2 实战案例:接口限流
importjava.util.concurrent.Semaphore;/** * Semaphore 实战:接口限流 */publicclassSemaphoreDemo{// 定义信号量,允许3个线程同时访问privatestaticfinalSemaphore semaphore =newSemaphore(3);// 模拟接口方法publicstaticvoidapiInvoke(String threadName)throwsInterruptedException{// 获取许可证 semaphore.acquire();try{System.out.println(threadName +":获取许可证,开始调用接口");Thread.sleep(1000);System.out.println(threadName +":接口调用完成");}finally{// 归还许可证 semaphore.release();System.out.println(threadName +":归还许可证,当前可用许可证:"+ semaphore.availablePermits());}}publicstaticvoidmain(String[] args){// 模拟10个线程并发调用接口for(int i =0; i <10; i++){int finalI = i;newThread(()->{try{apiInvoke("线程"+ finalI);}catch(InterruptedException e){ e.printStackTrace();}}).start();}}}
1.3.3.3 适用场景
  • 接口限流,控制并发访问数,防止系统过载。
  • 资源池访问控制,如数据库连接池、线程池的资源分配。

1.4 实战案例:基于并发容器实现生产者消费者模式

生产者消费者模式是并发编程中的经典模式。它通过阻塞队列解耦生产者和消费者,平衡生产和消费速度。

1.4.1 需求分析

  1. 生产者线程生产商品,将商品放入阻塞队列。
  2. 消费者线程从阻塞队列中取出商品进行消费。
  3. 当队列满时,生产者阻塞;当队列空时,消费者阻塞。
  4. 支持多个生产者和多个消费者并发执行。

1.4.2 代码实现

importjava.util.concurrent.ArrayBlockingQueue;importjava.util.concurrent.BlockingQueue;importjava.util.concurrent.TimeUnit;/** * 实战:基于 BlockingQueue 实现生产者消费者模式 */publicclassProducerConsumerPattern{// 定义有界阻塞队列,容量为5privatestaticfinalBlockingQueue<Product> queue =newArrayBlockingQueue<>(5);// 商品实体类staticclassProduct{privateString id;privateString name;publicProduct(String id,String name){this.id = id;this.name = name;}@OverridepublicStringtoString(){return"Product{id='"+ id +"',+ name +"'}";}}// 生产者类staticclassProducerimplementsRunnable{privateString producerName;publicProducer(String producerName){this.producerName = producerName;}@Overridepublicvoidrun(){int count =1;while(true){try{Product product =newProduct("P"+ count,"商品"+ count); queue.put(product);System.out.println(producerName +" 生产:"+ product +",队列当前大小:"+ queue.size()); count++;// 模拟生产耗时TimeUnit.SECONDS.sleep(1);}catch(InterruptedException e){ e.printStackTrace();Thread.currentThread().interrupt();}}}}// 消费者类staticclassConsumerimplementsRunnable{privateString consumerName;publicConsumer(String consumerName){this.consumerName = consumerName;}@Overridepublicvoidrun(){while(true){try{Product product = queue.take();System.out.println(consumerName +" 消费:"+ product +",队列当前大小:"+ queue.size());// 模拟消费耗时TimeUnit.SECONDS.sleep(2);}catch(InterruptedException e){ e.printStackTrace();Thread.currentThread().interrupt();}}}}publicstaticvoidmain(String[] args){// 启动2个生产者线程newThread(newProducer("生产者1")).start();newThread(newProducer("生产者2")).start();// 启动3个消费者线程newThread(newConsumer("消费者1")).start();newThread(newConsumer("消费者2")).start();newThread(newConsumer("消费者3")).start();}}

1.4.3 运行结果分析

  • 当队列满时,生产者线程会阻塞,直到消费者消费商品腾出空间。
  • 当队列空时,消费者线程会阻塞,直到生产者生产新的商品。
  • 多个生产者和消费者可以并发执行,系统运行稳定,不会出现数据错乱。

实战结论:基于 BlockingQueue 实现生产者消费者模式,无需手动加锁和控制线程状态。这种方式代码简洁、性能稳定,是并发编程中的优选方案。

1.5 并发容器与协作工具选型建议

工具类型具体实现核心优势适用场景
并发MapConcurrentHashMap高并发读写、细粒度锁缓存存储、键值对数据共享
并发ListCopyOnWriteArrayList读操作无锁、性能高读多写少、配置列表
阻塞队列ArrayBlockingQueue/LinkedBlockingQueue自动阻塞、解耦生产者消费者任务队列、消息传递
线程协作CountDownLatch主线程等待子线程完成任务汇总、系统启动
线程协作CyclicBarrier线程组相互等待、可重复使用数据分片处理、批量任务
限流工具Semaphore控制并发访问数接口限流、资源池管理

1.6 本章小结

💡 本章重点讲解了JAVA中常用的并发容器和线程协作工具。包括 ConcurrentHashMapCopyOnWriteArrayListBlockingQueue 的核心原理与使用方法,以及 CountDownLatchCyclicBarrierSemaphore 三个协作工具的实战场景。
💡 通过生产者消费者模式的完整案例,掌握了如何结合并发容器和协作工具,实现高效、安全的并发编程。
✅ 并发容器和协作工具是JAVA并发编程的核心组件。合理选择和使用这些工具,能够大幅降低并发编程的复杂度,提升系统的稳定性和性能。

Read more

SpringBoot+Vue 乡村政务办公系统平台完整项目源码+SQL脚本+接口文档【Java Web毕设】

SpringBoot+Vue 乡村政务办公系统平台完整项目源码+SQL脚本+接口文档【Java Web毕设】

摘要 随着乡村振兴战略的深入推进,乡村政务管理的信息化需求日益增长。传统的乡村政务办公模式存在效率低下、信息孤岛、数据共享困难等问题,亟需借助现代信息技术实现数字化转型。乡村政务办公系统平台旨在整合乡村政务资源,提高办公效率,促进政务公开,优化村民服务体验。该系统通过信息化手段实现村务管理、政策宣传、帮扶信息管理等功能,为乡村治理现代化提供技术支撑。关键词:乡村振兴、政务信息化、数字治理、村务管理、办公系统。 本系统基于SpringBoot+Vue技术栈开发,采用前后端分离架构,后端使用SpringBoot框架实现RESTful API接口,前端采用Vue.js构建用户界面,数据库选用MySQL存储数据。系统功能涵盖用户权限管理、新闻公告发布、帮扶信息管理、村民信息登记等模块,支持多角色登录和权限控制。接口文档采用Swagger生成,便于开发调试。系统通过高内聚低耦合的设计理念,确保代码可维护性和扩展性,为乡村政务办公提供高效、便捷的解决方案。关键词:SpringBoot、Vue.js、RESTful API、MySQL、Swagger。 数据表设计 帮扶信息数据表

By Ne0inhk
中秋满月皆十六圆?Java实证求解后的真相

中秋满月皆十六圆?Java实证求解后的真相

目录 前言 一、天文上的满月 1、形成原理及定义 2、出现时间及观测 3、文化意义 二、Java模拟月满计算 1、整体实现逻辑 2、主计算方法详解 3、核心天文算法详解 3.1 儒略日计算基础 3.2 时间参数计算 3.3 天文参数计算 3.4 周期项修正计算 4、辅助方法详解 4.1 角度标准化 4.2 日历与儒略日转换 4.3 儒略日转日历 三、近年中秋满月计算及对比 1、近年中秋满月计算 2、近年计算与公布时间对比 四、总结 前言

By Ne0inhk
IDEA安装教程配置java环境(超详细)_idea配置java,零基础入门到精通,收藏这篇就够了

IDEA安装教程配置java环境(超详细)_idea配置java,零基础入门到精通,收藏这篇就够了

引言 IntelliJ IDEA 是一款功能强大的集成开发环境(IDE),广泛用于 Java 开发,但也支持多种编程语言,如 Kotlin、Groovy 和 Scala。本文将为你提供一步一步的指南,帮助你在 Windows 系统上顺利安装 IntelliJ IDEA。 一、安装 JDK 1.1下载JDK 1.访问 JDK 下载页面 打开浏览器,访问Oracle JDK 下载页面. Java Downloads | Oraclehttps://www.oracle.com/java/technologies/downloads/#java22 2.选择版本 选择适合你的 JDK 版本(例如 JDK17或JDK21

By Ne0inhk
【JavaSE-网络部分04】网络原理-传输层:UDP + TCP 可靠性三大核心机制(确认应答 / 超时重传 / 连接管理)

【JavaSE-网络部分04】网络原理-传输层:UDP + TCP 可靠性三大核心机制(确认应答 / 超时重传 / 连接管理)

传输层的学习 传输层我们说过最核心的协议是TCP和UDP。 那么在这里面我们再谈一下端口号。 再谈端口号 我们说端口号是用整数表示,用来区分同一台主机上不同的应用程序。 我们前面在网络编程冲每个程序中的socket创建的时候都需要关联端口号,那么对于服务器来说,端口号是程序员的手动指定的;而对于我们的客户端来说,端口号是系统自动分配的。 端口号是由两个字节表示的无符号整数 * 范围:0~65535。 虽然它的范围呢比较多,但是呢并不是所有的数都能是可以使用的。 * 0~1023 这样的范围通常我们是不使用的,他们叫做知名端口号,是给一些知名的服务器预留的。 虽然现在我们知名的服务器没有太多,已经寥寥无几了,但是呢有两个知名的端口,一定要重点认识。 * 80 ==> 这个是给HTTP服务器留的端口号。 * 443 ==》 这个是给HTTPS服务器留的端口。 问题1:一个进程是否可以绑定多个端口号? 答:这个是完全可以的,但是注意其实不是进程绑定端口号,而是我们的socket绑定端口,我们一个进程中完全可以创建多个socket,所以呢可以同时关联到多个端口号

By Ne0inhk