Java多线程(九)ConcurrentHashMap && CopyOnWriteArrayList && CopyOnWriteArraySet

306 阅读4分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

ConcurrentHashMap

为什么需要 ConcurrentHashMap

常用的HashMap在多线程情况下,在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,但是这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

而一个键值存储集合HashTable,它是线程安全的,它在所有涉及到多线程操作的都加上了synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争一把锁,如线程1使用put进行元素的添加,那么线程2不但不能使用put操作,也不能使用get操作获得元素。在多线程的环境下,它是安全的,但是无疑是效率低下的。

对于ConcurrentHashMap来说,它使用了锁分段技术,它首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他的段数据也能够被其他线程访问。

ConcurrentHashMap 的结构

ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则使用键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry,这个和HashMap的数据存储结构一样。

在这里插入图片描述

get操作

ConcurrentHashMap 的get操作高效之处在于整个get过程不用加锁,除非读到的数据是空才会加锁重读。ConcurrentHashMap的get操作跟HashMap类似,ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。

put操作

因为put需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后再Segment进行插入操作。插入操作需要经过两个步骤:1)判断是否需要对Segment里的HashEntry数组进行扩容,第二步需要定位添加元素的位置,然后放进HashEntry数组里。值得注意的时,为了高效,ConcurrentHashMap 不会对整个容器进行扩容,而只对某个Segment进行扩容。

size操作

要统计整个ConcurrentHashMap 里元素的个数,就必须统计所有Segment里元素的个数之后求和,最安全的操作是统计size的时候把所有Segment的put、remove、clean方法全部锁住,但是这种方法的效率太低了!所以ConcurrentHashMap 操作的先尝试2次通过不锁住Segment的方式统计各个Segment大小,如果统计过程中,count发生了变化,则再采用加锁的方式来统计所有Segment的大小。

但是ConcurrentHashMap 是如何判断在统计过程中容器的count发生变化呢?使用一个变量modCount变量,在put、remove、clean方法里操作元素前都会对modCount进行加1,那么在统计size前后modCount是否变化即可。

CopyOnWriteArrayList

Java中的ArrayList是个多线程非安全的容器,这在Java多线程(三) 多线程不安全的典型例子 - 掘金 (juejin.cn)中有介绍,解决之一的办法是使用锁来保证共享数据一次只被一个线程操作(synchronized、lock),在JUC中可以用 CopyOnWriteArrayList 来代替 ArrayList。

public class ConcurrentTools {
    public static void main(String[] args) {
        List<Integer> list = new CopyOnWriteArrayList<Integer>();
        new Thread(()->{
            for (int i=0;i<60;i++)
            {
                list.add(list.size());
                System.out.println(Thread.currentThread().getName()+" 在第"+list.size()+" 个位置添加了一项,现在大小是 "+list.size());
            }
        },"A").start();

        new Thread(()->{
            for (int i=0;i<60;i++)
            {
                list.add(list.size());
                System.out.println(Thread.currentThread().getName()+" 在第"+list.size()+" 个位置添加了一项,现在大小是 "+list.size());
            }
        },"B").start();
    }
}

在这里插入图片描述

CopyOnWriteArraySet

与 CopyOnWriteArrayList 类似,在JUC也有HahSet的替代容器——CopyOnWriteArraySet,他也是多线程安全的。

public class ConcurrentTools {

    public static void main(String[] args) {
        Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 30; i++) {

            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}

在这里插入图片描述