2026/4/5 17:36:23
网站建设
项目流程
上海网络平台网站建设,wordpress模板调用文件夹下,商丘的互联网公司,郑州今天的最新消息不出意外这是多线程的最后一篇文章#xff0c;主要介绍的是面试中比较常考的一个点——多线程下使用容器#xff0c;我们开始吧~我们知道#xff0c;在单线程环境下ArrayList、HashMap等容器使用起来非常方便#xff0c;但在多线程环境中#xff0c;如果多个线程同时对容器…不出意外这是多线程的最后一篇文章主要介绍的是面试中比较常考的一个点——多线程下使用容器我们开始吧~我们知道在单线程环境下ArrayList、HashMap等容器使用起来非常方便但在多线程环境中如果多个线程同时对容器进行修改就可能导致数据不一致、数组越界甚至死循环等问题那么在多线程环境下我们该如何安全地使用这些容器呢1. 多线程下使用ArrayList首先最直接也最简单的方式加锁ListIntegerlistnewArrayList();synchronized(list){list.add();}这样写显然是可行的也很灵活但是对代码的侵入性强、且很容易出错一旦出错年终奖就没了因此在实际开发过程中更常见的做法是使用Java提供的线程安全容器对并发控制进行统一封装尽量把坑留给框架而不是自己Collections.synchronizedList会返回一个线程安全的List其内部通过在关键方法上加上synchronized来保证线程安全虽然在一定程度上保证了线程安全但是由于所有操作共用一把锁并发度低在读多写少场景下性能较一般比较适合低并发场景除了Collections.synchronizedListJava还提供了另一种思路的线程安全list——CopyOnWriteArrayList从名字就能看出来它采用的是一种非常经典的并发设计思想写时拷贝写时拷贝的核心思路是读操作不加锁写操作时先拷贝一份底层数组在新数组上完成修改最后一次性替换引用好处很明显实现了读写分离写操作不会影响正在进行的读操作读操作不加锁不会阻塞并发性能很高写操作内部使用ReentrantLock保证线程安全同样它也存在问题写操作时需要拷贝数组内存开销较大如果list本身很大或者写操作频繁性能会明显下降多个线程同时写入写操作仍会互相竞争锁对比一下方案并发度侵入性适用场景自行加锁低高临时方案synchronizedList低低低并发CopyOnWriteArrayList高读低读多写少2. 多线程环境下使用队列在多线程环境中队列往往承担着线程协作的角色例如经典的生产者-消费者模型Java 在 java.util.concurrent 包中提供了一组 阻塞队列BlockingQueue 的实现用于简化这类并发场景阻塞队列的核心特性是队列为空时take()阻塞队列已满时put()阻塞以此避免频繁的轮询和手动加锁2.1 常见的BlockingQueue实现ArrayBlockingQueue基于数组实现容量固定内存连续结构简单LinkedBlockingQueue基于链表实现可以指定容量吞吐量高线程池中默认使用的就是这种队列实现PriorityBlockingQueue基于堆实现支持元素优先级出队顺序由优先级决定而不是FIFO适用于任务有明显优先级区分的场景TransferQueue支持直接把元素交给消费者如果没有消费者才会进入队列更强调线程之间的“交接”使用场景相对较少但在高并发任务调用中性能表现优秀3. 多线程环境下使用Map和 ArrayList 类似HashMap 在单线程环境下使用非常方便但在多线程环境中却是典型的线程不安全容器如果多个线程同时对HashMap进行put/resize等操作可能会导致数据覆盖、链表结构被破坏、JDK7中可能出现死循环等3.1 HashTableHashTable是Java早期提供的线程安全Map其实现方法也很直接给几乎所有public方法加上了synchronized这确实保证了线程安全但问题同样明显所有操作共用一把锁、并发度较低、在高并发场景下性能较差因此HashTable基本上只存在于学校教材中实际开发中很少使用3.2 ConcurrentHashMap并发环境下的首选它的设计目标非常明确在保证线程安全的前提下尽可能提高并发访问性能为此它主要做了三方面的优化① 细化锁粒度从“锁整张表”到“锁单个桶”JDK8中写操作只锁当前桶链表或红黑树不同桶上的操作可以并发进行这样一来只有在操作同一个桶时线程才可能发生阻塞并发性能大幅提升②原子操作维护sizeQ: 为什么size在并发下这么难A: 多个线程同时size读size的线程可能看到中间状态如果给size加全局锁每次put/remove都要竞争性能急剧下降ConcurrentHashMap内部不是使用一个size而是多个计数单元类似baseCount// 基础计数counterCells[]// 多个计数槽可以理解为使用多个小本子记账而不是所有人挤在一本账上写操作时尽量“就近记账”通过原子操作CAS对计数进行更新在并发冲突较大时将计数分散到多个计数单元中不同线程更新不同计数单元从而减少竞争在调用size()时会将各个计数单元的值进行累加得到当前Map的元素数量。由于统计过程中可能存在并发写入size()返回的是一个瞬时近似值但在绝大多数业务场景下是可以接收的这样设计在避免全局锁的同时显著提升了高并发下的整体性能③渐进式rehash扩容是Map中开销最大的操作之一如果扩容时一次性创建新数组将所有元素整体搬迁那么在高并发场景下性能会非常糟糕ConcurrentHashMap的做法是新表和旧表同时存在扩容过程被“拆散”到后续的多次操作中完成具体表现为每次put/get/remove时顺带迁移一小部分旧数据直到所有桶都完成迁移蚂蚁搬家一点一点搬这种渐进式扩容的方式有效避免了长时间阻塞综上上上所述ConcurrentHashMap的优势主要在于锁粒度小、并发度高、针对热点操作做了大量优化、在高并发场景下性能稳定可靠因此在多线程环境中除非有非常明确的理由否则应该优先选择ConcurrentHashMap而不是HashMap或者HashTable完结撒花★,°:.☆(▽)/$:.°★。