JUC P7 线程安全集合 基础+代码

JUC P7 线程安全集合 基础+代码

教程:https://www.bilibili.com/video/BV16J411h7Rd

13. 线程安全集合类

13.1 概述

13.1.1 老古董

HashTableVector

13.1.2 使用 Collections 修饰的线程安全类(装饰器模式的思想)

在这里插入图片描述

13.1.3 JUC 安全集合类
13.1.3.1 CopyOnWrite

修改开销相对较重,基于 synchronized,适用于读多写少的场景
在这里插入图片描述

13.1.3.2 Blocking

大部分实现基于锁(ReentrantLock),并提供用来阻塞的方法
在这里插入图片描述

13.1.3.3 Concurrent

内部很多操作使用 CAS 优化,一般可以提供较高的吞吐量
在这里插入图片描述

  • 弱一致性
    • 遍历时弱一致性,例如,当利用迭代器遍历时,若容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
    • 求容量大小弱一致性,size() 操作未必 100% 准确
    • 读取弱一致性

Note:
对于非安全容器来讲,遍历过程中发生修改,使用了 fail-fast 的机制会让遍历立刻失败,抛出 ConcurrentModificationException,不再继续遍历。

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
new Thread(() -> {
    list.forEach(System.out::println);
}).start();
new Thread(() -> list.add(6)).start();
new Thread(() -> list.add(7)).start();
new Thread(() -> list.add(8)).start();

在这里插入图片描述

13.2 ConcurrentHashMap

13.2.1 练习:单词计数

随机生成 26 个字母,每个字母生成 200 次,打乱后装入 26 个文件中。

static final String ALPHA = "abcedfghijklmnopqrstuvwxyz";

public static void main(String[] args) {
    int length = ALPHA.length();
    int count = 200;
    List<String> list = new ArrayList<>(length * count);
    for (int i = 0; i < length; i++) {
        char ch = ALPHA.charAt(i);
        for (int j = 0; j < count; j++) {
            list.add(String.valueOf(ch));
        }
    }
    Collections.shuffle(list);
    for (int i = 0; i < 26; i++) {
        try (PrintWriter out = new PrintWriter(
                new OutputStreamWriter(
                        new FileOutputStream("G://tmp/" + (i + 1) + ".txt")))) {
            String collect = String.join("\n", list.subList(i * count, (i + 1) * count));
            out.print(collect);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

使用 HashMap 统计,可以看到结果不正确,正确应该是每个字母出现次数都是 200:

public static void main(String[] args) {
    demo(
        // 创建 map 集合
        // 创建 ConcurrentHashMap 对不对?
        () -> new HashMap<String, Integer>(),
        // 进行计数
        (map, words) -> {
            for (String word : words) {
                Integer counter = map.get(word);
                int newValue = counter == null ? 1 : counter + 1;
                map.put(word, newValue);
            }
        }
    );
}

private static <V> void demo(Supplier<Map<String, V>> supplier,
                             BiConsumer<Map<String, V>, List<String>> consumer) {
    Map<String, V> counterMap = supplier.get();
    List<Thread> ts = new ArrayList<>();
    for (int i = 1; i <= 26; i++) {
        int idx = i;
        Thread thread = new Thread(() -> {
            List<String> words = readFromFile(idx);
            consumer.accept(counterMap, words);
        });
        ts.add(thread);
    }
    ts.forEach(Thread::start);
    ts.forEach(t -> {
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    System.out.println(counterMap);
}

public static List<String> readFromFile(int i) {
    ArrayList<String> words = new ArrayList<>();
    try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("G://tmp/"
            + i + ".txt")))) {
        while (true) {
            String word = in.readLine();
            if (word == null) {
                break;
            }
            words.add(word);
        }
        return words;
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

在这里插入图片描述

ConcurrentHashMap 错误用法

假如把 HashMap 换成 ConcurrentHashMap 正确吗?

() -> new ConcurrentHashMap<String, Integer>()

在这里插入图片描述
可以看到结果也不正确!什么情况?

  • 因为只能保证一个方法是原子的,当多个方法交错使用,那么这个整体并不保证是原子的
    在这里插入图片描述
解决方案 1 (上 synchronized 锁)
() -> new ConcurrentHashMap<String, Integer>(),
// 进行计数
(map, words) -> {
    for (String word : words) {
        synchronized (map) {
            Integer counter = map.get(word);
            int newValue = counter == null ? 1 : counter + 1;
            map.put(word, newValue);
        }
    }
}

这样确实能解决问题,但是并发度不高。

解决方案 2(putIfAbsent 和 computeIfPresent)
() -> new ConcurrentHashMap<String, Integer>(),
// 进行计数
 (map, words) -> {
     for (String word : words) {
         map.putIfAbsent(word, 0);
         map.computeIfPresent(word, (key, value) -> value + 1);
     }
 }
解决方案 3 (LongAdder 累加器)
() -> new ConcurrentHashMap<String, LongAdder>(),
// 进行计数
(map, words) -> {
    for (String word : words) {
        LongAdder value = map.computeIfAbsent(word, (key) -> new LongAdder());
        value.increment();
    }
}
对比

经过大量测试,大部分情况都是第三种方案更胜一筹,第一种方案次之,最差的就是第二种方案:

在这里插入图片描述

12.2.2 JDK 7 HashMap 并发死链

比较形象的例子:jdk1.7中 hashmap为什么会发生死链? - 磊哥的回答 - 知乎

12.2.3 JDK 8 HashMap 并发数据不一致

两个线程同时插入尾节点,出现顶替的情况,造成数据丢失。以及两个线程同时修改数据的情况。

12.2.4 为什么 ConcurrentHashMap 不允许插入的 key/value 为 null ?

第一,代码中不允许!
在这里插入图片描述

第二,这会带来并发场景下的歧义:

举个例子:

  • 单线程下,比如使用 map.get(key) 方法后的结果返回 null,我们要判断 ① key 对应的值是 null,② 还是 map 中没有这个 key,有这两种情况。这时候我们可以使用 map.containsKey(key) 方法看返回的是 true 还是 false。如果是 true,代表 key 对应的值为 null,如果是 false,说明 map 中没有这个 key。单线程情况下确实可以使用额外的方法判定消除歧义。
  • 多线程下,判断是否含有 key 和获取 key 对应的值两个方法因为是分开的操作,这样无法保证一个线程下执行两个方法过程中是否有另外一个线程进行捣乱。假如说初始状态下 mapkey 对应 "hello"
时刻线程1线程2
1map.containsKey(key) 返回 true
2map.put(key, null)
3map.get(key),期待:"hello",实际:null

13.3 CopyOnWriteArrayList

底层实现采用了写入时拷贝的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其他线程的并发读读写分离

读-读,读-写都可以并发执行,写-写是互斥的。

适用于读多写少的场景。写过程太多会一直创建新的数组,典型的以空间换时间。

弱一致性
@SneakyThrows
public static void main(String[] args) {
    CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    Iterator<Integer> iter = list.iterator();
    new Thread(() -> {
        list.remove(0);
        System.out.println(list);
    }).start();
    
    TimeUnit.SECONDS.sleep(1);
    
    while (iter.hasNext()) {
        System.out.println(iter.next());
    }
}

在这里插入图片描述
弱一致性:

  • MySQL 的 MVCC
  • 高并发和一致性是矛盾的,需要权衡
  • 将读写分离
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哇咔咔负负得正

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值