Java并发集合

简介: 传统类集框架的弊端 1.并发集合的类型 2.并发单值集合 3.并发多值集合 4.跳表集合


 

传统类集框架的弊端

传统的类集框架存在一个非常严重的弊端。那就是在多线程的情况下对集合修改会报错。

如下代码

package Example2123;
import java.util.ArrayList;
import java.util.List;
public class javaDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
//        多线程
        for (int i =0;i<10;i++){
//            多线程下多次放入数据与输出数据
            new Thread(()->{
                for (int j=0;j<10;j++){
                    list.add(Thread.currentThread().getName());
                    System.out.println(list);
                }
            },"Thread"+i).start();
        }
    }
}

image.gif

image.gif编辑

出现了修改异常,因为传统的类集框架是针对单线程设计的。如果出现多线程同时修改的情况下再输出则会出现异常。为此JUC中就提供了并发集合来解决集合安全的问题


1.并发集合的类型

No. 集合接口 集合类 描述
1 List CopyOnWriteArrayList 当于线程安全的 ArrayList,并支持高并发访问
2 Set CopyOnWriteArraySet 相当于线程安全的 HashSet,基于 CopyOnWriteArrayList 实现
3 Set ConcurrentSkipListSet 相当于线程安全的 TreeSet,基于跳表结构实现,并支持高并发访问
4 Map ConcurrentHashMap 相当于线程安全的 HashMap,支持高并发访问
5 Map ConcurrentSkipListMap 相当于线程安全的 TreeMap,基于跳表结构实现,并支持高并发访问
6 ArrayBlockingQueue 基于数组实现的线程安全的有界阻塞队列
7 Queue LinkedBlockingQueue 单向链表实现的阻塞队列,支持 FIFO 处理
8 Queue ConcurrentLinkedQueue 单向链表实现的无界队列,支持 FIFO 处理
9 Deque LinkedBlockingDeque 双向链表实现的双向并发阻塞队列,支持 FIFO、FILO 处理
10 Deque ConcurrentLinkedDeque 双向链表实现的无界队列,支持 FIFO、FILO 处理

2.并发单值集合

并发单值在类集框架中分为两类,一类是List链表,一条是Set集合。以下分别是并发链表和并发集合

1.CopyOnWriteArraylist (并发链表)

常用方法如下:

方法名 描述
boolean add(E e) 将指定元素追加到此列表的末尾。
void add(int index, E element) 将指定元素插入此列表中的指定位置。
boolean remove(Object o) 从列表中移除指定元素的第一个匹配项(如果存在)。
E remove(int index) 移除列表中指定位置的元素。
E get(int index) 返回列表中指定位置的元素。
int size() 返回列表中的元素数。
boolean contains(Object o) 如果列表包含指定的元素,则返回 true
Iterator<E> iterator() 返回在列表的元素上进行迭代的迭代器。

案例代码:

package Example2124;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class javaDemo {
    public static void main(String[] args) {
//        创建并发链表
        List<String> list = new CopyOnWriteArrayList<>();
//        创建多线程
        for (int i=0;i<10;i++){
            new Thread(()->{
//                多线程对并发集合进行修改
                for (int j=0;j<10;j++){
                    list.add(Thread.currentThread().getName());
                    System.out.println(list);
                }
            },"Thread"+i).start();
        }
    }
}

image.gif

注意:

    1. 由于CopyOnWriteArrayList是基于数组实现的并发集合类。 在执行修改操作时候都会创建一个新的数组,再将更新后的数据复制进入新的数组中。所以该类实现修改的效率比较低。
    2. 在使用迭代器iterator的时候,并不支持iterato的remove删除元素的操作。

    2.CopyOnWriteArraySet

    以下是常用的方法

    方法名 描述
    boolean add(E e) 将指定元素添加到此集合中,如果已存在则不添加。
    boolean remove(Object o) 从集合中移除指定元素的第一个匹配项(如果存在)。
    boolean contains(Object o) 如果集合包含指定的元素,则返回 true
    int size() 返回集合中的元素数。
    boolean isEmpty() 如果集合不包含任何元素,则返回 true
    void clear() 从集合中移除所有元素。
    Iterator<E> iterator() 返回在集合的元素上进行迭代的迭代器。

    案例代码:

    package Example2125;
    import java.util.Set;
    import java.util.concurrent.CopyOnWriteArraySet;
    public class javaDemo {
        public static void main(String[] args) {
    //        创建并发集合
            Set<String> set = new CopyOnWriteArraySet<>();
    //        多个人同时写自己喜欢的Hobby
            String hobbies[] = new String[]{"篮球","逛街","打游戏","跑步","写编程"};
            for (int i=0;i<10;i++){
                new Thread(()->{
                    for (int j=0;j< hobbies.length;j++){
                        set.add(hobbies[j]);
                    }
                }).start();
            }
            System.out.println(set);
        }
    }

    image.gif

    image.gif编辑

    注意:CopyOnWriteArraySet是依赖于CopyOnWriteArrayList进行数据保存的,所以对应的迭代器输出时候也不能出现删除操作


    3.并发多值集合

    1.ConcurrentHashMap

    原理:将一个哈希表的结构分为多个片段,并且每一个片段中都有互斥锁,所以在同一个片段中线程之间是互斥的,而由于由多个片段,所以可以进行异步处理。这样保证了安全的前提下又能保证

    常用的方法

    方法名 描述
    V put(K key, V value) 将指定的键值对添加到映射中。如果键已经存在,则替换对应的值,并返回旧值。
    V get(Object key) 返回指定键所映射的值,如果键不存在,则返回 null
    boolean containsKey(Object key) 如果映射包含指定键的映射关系,则返回 true
    boolean containsValue(Object value) 如果映射将一个或多个键映射到指定值,则返回 true
    V remove(Object key) 从映射中移除指定键的映射关系,并返回该键对应的值。
    boolean remove(Object key, Object value) 仅当指定的键当前映射到指定的值时,才从映射中移除该键的映射关系。
    int size() 返回映射中键值对的数目。
    boolean isEmpty() 如果映射不包含任何键值对,则返回 true
    void clear() 从映射中移除所有的键值对。

    案例代码:

    package Example2126;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    public class javaDemo {
        public static void main(String[] args) {
    //        创建并发哈希表
            Map<String,Integer> map = new ConcurrentHashMap<>();
            for (int i=0;i<5;i++){
                new Thread(()->{
    //                多线程传输数据
                    for (int j = 0;j<5;j++){
                        map.put(Thread.currentThread().getName(),j);
                    }
                },"Thread"+i).start();
            }
    //        输出map
            System.out.println(map);
        }
    }

    image.gif

     

    image.gif编辑

     

    面试题哪些集合类是线程安全的?

      • Vector:就比Arraylist多了个同步化机制(线程安全)。
      • Stack:栈,也是线程安全的,继承于Vector。
      • Hashtable:就比Hashmap多了个线程安全。
      • ConcurrentHashMap:是一种高效但是线程安全的集合。

      面试题 Java8开始ConcurrentHashMap,为什么舍弃分段锁?

      ConcurrentHashMap的原理是引用了内部的 Segment ( ReentrantLock )  分段锁,保证在操作不同段 map 的时候, 可以并发执行, 操作同段 map 的时候,进行锁的竞争和等待。从而达到线程安全, 且效率大于 synchronized。

      但是在 Java 8 之后, JDK 却弃用了这个策略,重新使用了 synchronized+CAS。

      弃用原因

      通过  JDK 的源码和官方文档看来, 他们认为的弃用分段锁的原因由以下几点:

        1. 加入多个分段锁浪费内存空间。
        2. 生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
        3. 为了提高 GC 的效率
        4. 新的同步方案
        5. 既然弃用了分段锁, 那么一定由新的线程安全方案, 我们来看看源码是怎么解决线程安全的呢?(源码保留了segment 代码, 但并没有使用)。

        面试题 ConcurrentHashMap(JDK1.8)为什么要使用synchronized而不是如ReentranLock这样的可重入锁?

        (1)锁的粒度

        首先锁的粒度并没有变粗,甚至变得更细了。每当扩容一次,ConcurrentHashMap的并发度就扩大一倍。

        (2)Hash冲突

        JDK1.7中,ConcurrentHashMap从过二次hash的方式(Segment -> HashEntry)能够快速的找到查找的元素。在1.8中通过链表加红黑树的形式弥补了put、get时的性能差距。

        JDK1.8中,在ConcurrentHashmap进行扩容时,其他线程可以通过检测数组中的节点决定是否对这条链表(红黑树)进行扩容,减小了扩容的粒度,提高了扩容的效率。

        下面是我对面试中的那个问题的一下看法。

        为什么是synchronized,而不是ReentranLock

        (1)减少内存开销

        假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。

        (2)获得JVM的支持

        可重入锁毕竟是API这个级别的,后续的性能优化空间很小。

        synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。这就使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。

         面试题 concurrentHashMap和HashTable有什么区别?

        concurrentHashMap融合了hashmap和hashtable的优势,hashmap是不同步的,但是单线程情况下效率高,hashtable是同步的同步情况下保证程序执行的正确性。

        concurrentHashMap锁的方式是细粒度的。concurrentHashMap将hash分为16个桶(默认值),诸如get、put、remove等常用操作只锁住当前需要用到的桶。

        concurrentHashMap的读取并发,因为读取的大多数时候都没有锁定,所以读取操作几乎是完全的并发操作,只是在求size时才需要锁定整个hash。

        而且在迭代时,concurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式,弱一致迭代器。在这种方式中,当iterator被创建后集合再发生改变就不会抛出ConcurrentModificationException,取而代之的是在改变时new新的数据而不是影响原来的数据,iterator完成后再讲头指针替代为新的数据,这样iterator时使用的是原来的数据。


        4.跳表集合

        跳表是一种类似于平衡二叉树的数据结构,主要在链表中使用

        原理:一个有序的数组正常使用索引查询的时候时间复杂度为O(1),但是如果是需要通过遍历查找想要优化查找速度则会使用二分法,而链表不是数组,为了有相似的效果所以引入了跳表集合

        跳表的两种集合类型:ConcurrentSkipListMap、ConcurrentSkipListSet,下面依次介绍

        1.ConcurrentSkipListMap

        常用方法:

        方法 描述
        put(key, value) 将指定的键值对插入到映射中。如果键已经存在,则更新对应的值。
        get(key) 返回与指定键关联的值,如果键不存在,则返回 null。
        remove(key) 从映射中移除与指定键关联的映射关系。
        containsKey(key) 判断映射中是否包含指定键。
        containsValue(value) 判断映射中是否包含指定值。
        size() 返回映射中键值对的数量。
        isEmpty() 判断映射是否为空。
        clear() 从映射中移除所有的键值对。

        案例代码:

        package Example2127;
        import java.util.Map;
        import java.util.concurrent.ConcurrentHashMap;
        import java.util.concurrent.CountDownLatch;
        public class javaDemo {
            public static void main(String[] args) throws Exception {
                // 创建子线程优先同步器
                CountDownLatch latch = new CountDownLatch(10);
                Map<String, Integer> map = new ConcurrentHashMap<>();
                for (int i = 0; i < 10; i++) {
                    int threadNum = i;
                    new Thread(() -> {
                        for (int j = 0; j < 10; j++) {
                            // 存放数据到map中
                            map.put("Thread" + threadNum + "-" + j, j);
                        }
                        latch.countDown();
                    }, "Thread" + threadNum).start();
                }
                // 等待子线程放入所有数据完毕
                latch.await();
                // 通过跳表的查询,优化查询速度
                System.out.println(map.get("Thread9-5"));
            }
        }

        image.gif

        image.gif编辑

        2.ConcurrentSkipListSet

        常用的方法:

        方法 描述
        boolean add(E e) 向集合中添加指定元素
        boolean remove(Object o) 从集合中移除指定元素
        boolean contains(Object o) 判断集合中是否包含指定元素
        int size() 返回集合的大小
        E first() 返回集合中的第一个元素
        E last() 返回集合中的最后一个元素
        Iterator<E> iterator() 返回在此集合元素上进行迭代的迭代器
        void clear() 从集合中移除所有元素
        Object[] toArray() 返回包含集合中所有元素的数组
        boolean isEmpty() 判断集合是否为空

        案例代码:

        package Example2128;
        import java.util.Set;
        import java.util.concurrent.ConcurrentSkipListSet;
        import java.util.concurrent.CountDownLatch;
        public class javaDemo {
            public static void main(String[] args) {
                Set<String> set = new ConcurrentSkipListSet<>();
                CountDownLatch latch = new CountDownLatch(10);
                for (int i = 0; i < 10; i++) {
                    final int temp = i;
                    new Thread(() -> {
                        for (int j = 0; j < 10; j++) {
                            set.add("Thread" + temp + "-" + j);
                        }
                        latch.countDown();
                    }, "Thread" + temp).start();
                }
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(set.contains("Thread3-8"));
            }
        }

        image.gif

        image.gif编辑


        目录
        相关文章
        |
        18天前
        |
        算法 Java 数据处理
        从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。
        从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。HashSet基于哈希表实现,提供高效的元素操作;TreeSet则通过红黑树实现元素的自然排序,适合需要有序访问的场景。本文通过示例代码详细介绍了两者的特性和应用场景。
        34 6
        |
        18天前
        |
        存储 Java
        深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
        【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
        33 3
        |
        18天前
        |
        存储 Java 数据处理
        Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位
        【10月更文挑战第16天】Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位。本文通过快速去重和高效查找两个案例,展示了Set如何简化数据处理流程,提升代码效率。使用HashSet可轻松实现数据去重,而contains方法则提供了快速查找的功能,彰显了Set在处理大量数据时的优势。
        28 2
        |
        20天前
        |
        存储 算法 Java
        Java Set因其“无重复”特性在集合框架中独树一帜
        【10月更文挑战第14天】Java Set因其“无重复”特性在集合框架中独树一帜。本文深入解析Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定的数据结构(哈希表、红黑树)确保元素唯一性,并提供最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的`hashCode()`与`equals()`方法。
        26 3
        |
        2天前
        |
        存储 Java
        判断一个元素是否在 Java 中的 Set 集合中
        【10月更文挑战第30天】使用`contains()`方法可以方便快捷地判断一个元素是否在Java中的`Set`集合中,但对于自定义对象,需要注意重写`equals()`方法以确保正确的判断结果,同时根据具体的性能需求选择合适的`Set`实现类。
        |
        2天前
        |
        存储 Java 开发者
        在 Java 中,如何遍历一个 Set 集合?
        【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
        |
        2天前
        |
        Java 开发者
        |
        14天前
        |
        安全 Java 程序员
        深入Java集合框架:解密List的Fail-Fast与Fail-Safe机制
        本文介绍了 Java 中 List 的遍历和删除操作,重点讨论了快速失败(fail-fast)和安全失败(fail-safe)机制。通过普通 for 循环、迭代器和 foreach 循环的对比,详细解释了各种方法的优缺点及适用场景,特别是在多线程环境下的表现。最后推荐了适合高并发场景的 fail-safe 容器,如 CopyOnWriteArrayList 和 ConcurrentHashMap。
        43 5
        |
        16天前
        |
        安全 Java 程序员
        Java集合之战:ArrayList vs LinkedList,谁才是你的最佳选择?
        本文介绍了 Java 中常用的两个集合类 ArrayList 和 LinkedList,分析了它们的底层实现、特点及适用场景。ArrayList 基于数组,适合频繁查询;LinkedList 基于链表,适合频繁增删。文章还讨论了如何实现线程安全,推荐使用 CopyOnWriteArrayList 来提升性能。希望帮助读者选择合适的数据结构,写出更高效的代码。
        43 3
        |
        18天前
        |
        存储 Java 数据处理
        Set 是 Java 集合框架中的一个接口,不包含重复元素且不保证元素顺序。
        【10月更文挑战第16天】Java Set:无序之美,不重复之魅!Set 是 Java 集合框架中的一个接口,不包含重复元素且不保证元素顺序。通过 hashCode() 和 equals() 方法实现唯一性,适用于需要唯一性约束的数据处理。示例代码展示了如何使用 HashSet 添加和遍历元素,体现了 Set 的高效性和简洁性。
        23 4