JUC并发集合

目录

传统类集框架的弊端

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();
        }
    }
}

 出现了修改异常,因为传统的类集框架是针对单线程设计的。如果出现多线程同时修改的情况下再输出则会出现异常。为此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();
        }
    }
}

注意:

  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);
    }
}

注意: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);
    }
}

面试题 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"));
    }
}

 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"));
    }
}

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

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

猜你喜欢

转载自blog.csdn.net/dogxixi/article/details/132314118