JUC学习之线程安全集合类

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC学习之线程安全集合类相关的知识,希望对你有一定的参考价值。


线程安全集合类概述

线程安全集合类可以分为三大类:

  • 遗留的线程安全集合如 Hashtable—线程安全的map实现 , Vector—线程安全的list实现



性能低,不建议使用

  • 使用 Collections 装饰的线程安全集合,如:
  1. Collections.synchronizedCollection
  2. Collections.synchronizedList
  3. Collections.synchronizedMap
  4. Collections.synchronizedSet
  5. Collections.synchronizedNavigableMap
  6. Collections.synchronizedNavigableSet
  7. Collections.synchronizedSortedMap
  8. Collections.synchronizedSortedSet

性能也不高,底层都是通过加同步锁实现的

  • java.util.concurrent.*

重点介绍 java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:Blocking、CopyOnWrite、Concurrent

  • Blocking 大部分实现基于锁,并提供用来阻塞的方法—基于ReentrantLock实现
  • CopyOnWrite 之类容器修改开销相对较重
  • Concurrent 类型的容器
  • 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
  • 弱一致性
  • 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍 历,这时内容是旧的
  • 求大小弱一致性,size 操作未必是 100% 准确
  • 读取弱一致性

遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出ConcurrentModificationException,不再继续遍历


ConcurrentHashMap

练习:单词计数

生成测试数据

package com;


import lombok.extern.slf4j.Slf4j;

import java.io.FileOutputStream;
import java.io.IOException;
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;

/**
 * @author 大忽悠
 * @create 2022/1/8 15:20
 */
@Slf4j
public class Main 
    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);
        //循环26次
        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);
        //循环26次
        for (int i = 0; i < 26; i++) 
            //try...withResource写法
            try (PrintWriter out = new PrintWriter(
                    new OutputStreamWriter(
                            new FileOutputStream("tmp/" + (i + 1) + ".txt"))))
            

                String collect = list.subList(i * count, (i + 1) * count).stream()
                        //收集起来---通过\\n换行,完成集合中每个元素的拼接,最后返回一个字符串
                        .collect(Collectors.joining("\\n"));
               //将上面拼接的字符串,输出到指定文件中
                out.print(collect);
             catch (IOException e) 
            
        
    


模版代码,模版代码中封装了多线程读取文件的代码

package com;


import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Supplier;

/**
 * @author 大忽悠
 * @create 2022/1/8 15:20
 */
@Slf4j
public class Main 
    
    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<>();
        
        //准备26个线程
        for (int i = 1; i <= 26; i++) 
            int idx = i;
            Thread thread = new Thread(() -> 
                //从文件中读取字符放入list集合中,然后返回
                List<String> words = readFromFile(idx);
                //进行消费
                consumer.accept(counterMap, words);
            );
            ts.add(thread);
        
        //启动26个线程
        ts.forEach(t -> t.start());
        
        //等待26个线程都执行完毕
        ts.forEach(t -> 
            try 
                t.join();
             catch (InterruptedException e) 
                e.printStackTrace();
            
        );
        //打印map集合中的数据
        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("tmp/"
                + i + ".txt")))) 
        
            while (true) 
               //每次读取一行数据,加入集合中,一行一个单词
                String word = in.readLine();
                if (word == null) 
                    break;
                
                words.add(word);
            
            return words;
         catch (IOException e) 
            throw new RuntimeException(e);
        
    


你要做的是实现两个参数

  • 一是提供一个 map 集合,用来存放每个单词的计数结果,key 为单词,value 为计数
  • 二是提供一组操作,保证计数的安全性,会传递 map 集合以及 单词 List

正确结果输出应该是每个单词出现 200 次

a=200, b=200, c=200, d=200, e=200, f=200, g=200, h=200, i=200, j=200, k=200, l=200, m=200, 
n=200, o=200, p=200, q=200, r=200, s=200, t=200, u=200, v=200, w=200, x=200, y=200, z=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);
                    
                
        );
    

有没有问题?请改进

a=194, b=196, c=197, d=194, e=196, f=195, g=197, h=197, i=191, j=192, k=195, l=196, m=196, n=197, o=193, p=194, q=195, r=196, s=193, t=193, u=197, v=196, w=197, x=195, y=195, z=191


参考解答1

    public static void main(String[] args) 
                demo(
                        () -> new ConcurrentHashMap<String, LongAdder>(),
                        (map, words) -> 
                            for (String word : words) 
                                // 注意不能使用 putIfAbsent,此方法返回的是上一次的 value,首次调用返回 null
                                map.computeIfAbsent(word, (key) -> new LongAdder()).increment();
                            
                        
                );
    

输出

a=200, b=200, c=200, d=200, e=200, f=200, g=200, h=200, i=200, j=200, k=200, l=200, m=200, n=200, o=200, p=200, q=200, r=200, s=200, t=200, u=200, v=200, w=200, x=200, y=200, z=200


参考解答2

  public static void main(String[] args) 
        demo(
                () -> new ConcurrentHashMap<String, Integer>(),
                (map, words) -> 
                    for (String word : words) 
                        // 函数式编程,无需原子变量
                        map.merge(word, 1, Integer::sum);
                    
                
        );
    

输出

a=200, b=200, c=200, d=200, e=200, f=200, g=200, h=200, i=200, j=200, k=200, l=200, m=200, n=200, o=200, p=200, q=200, r=200, s=200, t=200, u=200, v=200, w=200, x=200, y=200, z=200

ConcurrentHashMap 原理

1. JDK 7 HashMap 并发死链

JDK7 HashMap的实现是数组加链表的方式来实现的

并发死链的现象只有在JDK7的环境下才会复现


注意

  • 要在 JDK 7 下运行,否则扩容机制和 hash 的计算方法都变了
  • 以下测试代码是精心准备的,不要随便改动
package com;


import lombok.extern.slf4j.Slf4j;

import java.util.HashMap;


/**
 * @author 大忽悠
 * @create 2022/1/8 15:20
 */
@Slf4j
public class Main 

    public static void main(String[] args) 
        // 测试 java 7 中哪些数字的 hash 结果相等
        System.out.println("长度为16时,桶下标为1的key");
        for (int i = 0; i < 64; i++) 
            if (hash(i) % 16 == 1) 
                System.out.println(i);
            
        
        System.out.println("长度为32时,桶下标为1的key");
        for (int i = 0; i < 64; i++) 
            if (hash(i) % 32 == 1) 
                System.out.println(i);
            
        
        // 1, 35, 16, 50 当大小为16时,它们在一个桶内
        final HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
        // 放 12 个元素
        map.put(2, null);
        map.put(3, null);
        map.put(4, null);
        map.put(5, null);
        map.put(6, null);
        map.put(7, null);
        map.put(8, null);
        map.put(9, null);
        map

以上是关于JUC学习之线程安全集合类的主要内容,如果未能解决你的问题,请参考以下文章

JUC学习之不可变

JUC学习之无锁---乐观锁(非阻塞)

JUC学习之无锁---乐观锁(非阻塞)

Java语言特性学习之四JUC

JUC并发编程 共享模式之工具 JUC 线程安全的集合类 -- 线程安全的集合类概述

JUC学习之Java 线程常用方法