线程安全的集合

JUC中的线程安全的集合

所谓的线程安全的集合,指的是集合中的每一个方法是原子操作,例如 get()put()等。但这并不表示使用了线程安全的集合就不会造成线程安全问题,正如事务不是简单地由一系列原子操作堆叠就可以实现的一样,只有正确地使用线程安全的集合,才能保证线程安全问题。

Blocking 类

Blocking 大部分基于锁,并提供阻塞的方法

CopyOnWrite 类

CopyOnWrite类的容器使用修改时拷贝,在修改时开销比较大,适用于读多写少的场景

Concurrent 类

内部使用CAS操作进行优化,一般可以提供比较高的吞吐量,性能相对较高

弱一致性

遍历时弱一致性

当利用迭代器进行遍历时,如果Concurrent容器发生修改,迭代出来的数据还是旧值。

对于非安全容器来讲,遍历时发生修改会利用 fail-fast 机制让遍历立刻失败,抛出 ConcurrentModificationException 异常

public class NoSafeCollectionMain {
    // 迭代次数应该设置得稍微大些, 保证在执行 forEach() 方法时还有线程没有加入到集合中, 这样才能正常显示错误
    public static final int ITER_COUNTS = 200;

    public static void main(String[] args) {
        // 使用非线程安全的集合类
        List<String> threadNames = new ArrayList<>();
        for (int i = 0; i < ITER_COUNTS; i++) {
            new Thread(() -> {
                threadNames.add(Thread.currentThread().getName());
            }, "thread-" + i).start();
        }
        // 主线程的 threadNames 进行遍历时, 可能其它线程对 threadNames 这个集合进行修改, 此时报错 ConcurrentModificationException
        threadNames.forEach(System.out::println);
    }
}

线程不安全的集合的fast-fail机制演示

求大小时弱一致性

size() 获取的值未必准确,例如另一个线程对容器进行修改

读取时弱一致性

使用 ConcurrentHashMap 进行 WordCount 案例

  1. 生成测试数据

    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.OutputStreamWriter;
    import java.io.PrintWriter;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class ConcurrentHashMapMain {
        public static final String ALPHA = "abcdefghijklmnopqrstuvwxyz";
        public static int COUNT = 200;
    
        public static void main(String[] args) {
            List<String> words = new ArrayList<>();
            for (char ch : ALPHA.toCharArray()) {
                for (int i = 0; i < COUNT; i++) {
                    words.add(String.valueOf(ch));
                }
            }
            Collections.shuffle(words);
    
            for (int i = 0; i < 26; i++) {
                PrintWriter printWriter = null;
                try {
                    printWriter = new PrintWriter(new OutputStreamWriter(new FileOutputStream("G:/java/juc/src/main/resources/words/" + (i + 1) + ".txt")));
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                }
                String collect = words.subList(i * COUNT, (i + 1) * COUNT)
                        .stream()
                        .collect(Collectors.joining("\n"));
                printWriter.print(collect);
                // 注意这里的flush(), 否则生成的文件中没有数据
                printWriter.flush();
            }
        }
    }
  1. 正确地使用 ConcurrentHashMap

    ConcurrentHashMap 相较于 synchronized 等锁的优势在于锁的粒度更细,可以提供更高的并发度,这是使用 ConcurrentHashMap 来替代 synchronized + HashMap 的原因。和 Redis 中通过 Lua 脚本来将多条非原子指令拼接成一条指令的操作类似,应该去寻找 ConcurrentHashMap 中是否提供某些方法,将多个方法拼接成一个原子方法。

    ConcurrentHashMap 提供在多线程环境下还能够正常使用的集合(不需要显式加锁),搭配 CAS 操作的其他类型或方法才能够完成正确的并发控制。

多线程下扩容时的并发死链问题

产生原因

  1. JDK7 中的 HashMap 在遇到哈希冲突时,使用头插法。

    这样可能会造成 e 节点两次被访问,第一次访问 e.next = null,第二次访问 e.next = next,而 next.next = e,则可能产生链表环路。

测试案例

  1. 需要在 JDK7 的环境下进行测试,因为 JDK7 时 HashMap 的拉链法是使用的头插法,而 JDK8 中使用的是尾插法。另外 JDK8 对于扩容机制和 hash 值的计算方法都发生的改变

  2. 测试代码

    • 找到扩容前和扩容后桶下标相同的key
    • 触发扩容:元素个数达到阈值(总容量的3/4),初始容量是16

   转载规则


《线程安全的集合》 熊水斌 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
02-JVM内存结构 02-JVM内存结构
JVM 内存结构共享区域和私有区域 相关虚拟机 VM 参数项StringTable 相关配置项 参数项 描述 -XX:+PrintStringTableStatistics 输出 StringTable 的统计信息 -XX:
2023-03-20
下一篇 
数仓 数仓
数仓项目项目准备技术选型 系统数据流程图 版本选型 集群资源规划 用户行为日志用户行为日志概述用户行为日志的内容,主要包含用户的各项行为信息以及行为所处的环境信息。收集这些信息的主要目的是为了优化产品和为各项分析统计指标提供数据支撑。
2023-03-16
  目录