[Java EE] 多线程进阶(3) [线程安全集合类]

五.Java 中线程安全的集合类

线程安全的集合类 , 其核心解决普通集合(如 ArrayList/HashMap)在并发读写时的数据错乱 , ConcurrentMidificationException , 此循环等问题

1.线程安全集合的核心分类

分类

实现原理

代表类

核心特点

同步包装类

Collections.synchronizedXxx()

synchronizedList/synchronizedMap

全局加锁(synchronized),简单但并发性能低

JUC 并发集合

分段锁 / CAS / 写时复制 / 阻塞队列

ConcurrentHashMap/CopyOnWriteArrayList/LinkedBlockingQueue

精细化锁 / 无锁设计,高并发下性能更优

2.多线程下使用 ArrayList

① 自己使用同步机制(synchronized 或 ReentrantLock)

此处不再说

② 通过 Collection 工具类为普通集合套上 [ 全局锁 ]

本质是对集合所有方法加 synchronized

List<String> syncList = Collections.synchronizedList(new ArrayList<>());

注意 : 迭代时需要手动加锁 , 否则抛异常(ConcurrentMidificationException)

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        //多线程下的ArraryList
        List<String> syncList = Collections.synchronizedList(new ArrayList<>());
        syncList.add("Hello");
        syncList.add("World");
        new Thread(new Runnable() {
            @Override
            public void run() {
                syncList.add("Java");
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                syncList.add("Thread");
            }
        }).start();
        Thread.sleep(1000);
        synchronized (syncList) {//必须锁定集合对象本身
            for (String s : syncList) {
                System.out.println(s);
            }

        }

    }
}

③ 使用 JUC 包 , CopyOnWriteArrayList

核心实现 :

  • 写操作 : (add/remove/set) 复制一份新的数组 , 在数组上修改 , 修改完成后替换原数组 , 全程加锁
  • 读操作 : (get/iterator) 直接读原数组 , 无锁 , 性能极高

常见问题 :

问题 1 : 写操作性能地 , 内存开销大

解决方案 : 仅用于都铎写少的场景 ; 当频繁场景时改用 ReentrantLock 手动加锁的普通 List

问题 2 : 迭代过程中其他线程的修改不会反映到迭代器中

解决方案 : 需要加锁 ; 或者每次迭代前重新获取集合

3.多线程使用队列

① ArrayBlockingQueue

基于数组实现的阻塞队列

② LinkedBlockingQueue

基于链表实现的阻塞队列

③ PriorityBlockingQueue

基于堆实现的带优先级的阻塞队列

④ TransferQueue

最多只包含一个元素的阻塞队列

4.多线程使用哈希表

在多线程下使用哈希表(键值对储存) , 核心问题是解决线程安全和并发性能--普通 HashMap 线程不安全 , 并发读写会导致数据错乱 , 死循环 , ConcurrentModificationException 等问题

① 线程安全哈希表

类型

代表类

实现原理

全局锁哈希表

Hashtable/

Collections.synchronizedMap

所有方法加 synchronized;全局锁

精细化锁哈希表

ConcurrentHashMap

JDK1.7:分段锁;JDK1.8:CAS + 局部synchronized

写时复制哈希表

ConcurrentSkipListMap(有序)

跳表 + CAS,无锁设计

②Hashtable(低效)

只是简单的把关键的方法加上了 synchronized

这相当于直接对 Hashtable 对象本身加锁

  • 如果多线程访问同一个 Hashtable 就会直接造成锁冲突
  • 一旦触发扩容 , 就由该线程完成整个扩容过程

注意 : Hashtable 是 JDK1.0 的老旧类 , 性能差 , 已被ConcurrentHashMap 完全替代 , 仅作为了解

②ConcurrentHashMap(首选)

核心原理 : 抛弃了 JDK1.7 的分段锁(把这些链表分成几组 , 每个组安排一个锁) , 改用更细粒度的锁机制 (锁桶)

  • 读操作 : 无锁 , 通过 volatile 保证数据可见性
  • 写操作 : 1)对空桶 : CAS 原子操作写入 , 无锁 ; 2) 对非空桶 : 对桶首届点加 synchronized 锁 , 仅阻塞当前桶的读写 , 其他桶可并发
  • 当桶元素超过 8 个转为红黑树 , 提升查找性能
private static final Map<String, Integer> map = new ConcurrentHashMap<>();
扩容机制 : 化整为零
  • 发现扩容的线程只创建新数组,搬几个元素 : 1) 触发扩容的线程先标记 , 并且创建新数组(容量翻倍) ; 2) 按步长 (16)拆分原数组 , 该线程仅迁移自己负责的一小段桶(目的:避免的那现场一次迁移所有元素导致长时间阻塞)
  • 扩容期间新老数组同时存在 : 1)新数组作为全局变量 , 扩容全程与老数组共存 ; 2)迁移完成的桶做标记 , 未迁移的桶仍在老数组中(保证连续性)
  • 后续线程搬家 , 各搬一小部分 : 1)任何线程执行 put/remove 时 , 若检测到扩容中 , 会先暂停自身操作 , 协助迁移 ; 2)每个线程仅迁移自己"认领的桶段"(避免重复)
  • 搬完最后一个元素删除老数组 : 1)所有桶迁移完成 , 主线程将老数组替换成新数组 , 并删除 (释放内存); 2)更新扩容阈值 , 并标记扩容完成
  • 插入只往新数组加 : 扩容期间 , put 操作先检查桶是否已经迁移 , 若已迁移 : 直接写入新数组 , 若未迁移 : 先协助迁移 , 再写入新数组(防止老数组数据冗余)
  • 查找需同时查找新老数组 : get 操作 , 先查老数组 , 若桶已迁移完(标记) , 则跳转到行数则查询 ; 若桶未迁移 : 直接查询老数组 ; 若新老数组都查不到 , 再返回 null(保证扩容期间读取数据不丢失 , 不遗漏)
评论 5
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值