JUC-4. 集合线程安全

想了解更多JUC的知识——JUC并发编程合集

1. List不安全

  • ArrayList在并发情况下是不安全的

    public class ListTest {
        public static void main(String[] args) {
    
            List<Object> arrayList = new ArrayList<>();
    
            for(int i=1;i<=10;i++){
                new Thread(()->{
                    arrayList.add(UUID.randomUUID().toString().substring(0,5));
                    System.out.println(arrayList);
                },String.valueOf(i)).start();
            }
        }
    }
    

    在这里插入图片描述

  • 解决方案:

    1. List list = new Vector<>();
      在这里插入图片描述

    2. List list = Collections.synchronizedList(new ArrayList<>());

      • Collections.synchronizedList(new ArrayList<>())源码:

        在这里插入图片描述

      • 这个方法会根据你传入的List是否实现RandomAccess这个接口来返回的SynchronizedRandomAccessList还是SynchronizedList.

      • SynchronizedList实现了List接口,并重写了里面的方法,将里面的方法用synchronized修饰,但是listIterator(),iterator()没有加,所以在使用的时候需要加上synchronized。(下面是部分源码)
        在这里插入图片描述

    3. List<String> list = new CopyOnWriteArrayList<>();

      • CopyOnWriteArrayList:写入时复制, COW 计算机程序设计领域的一种优化策略

      • 特征:

        1. 线程安全的,多线程环境下可以直接使用,无需加锁

        2. 通过锁 + 数组拷贝 + volatile 关键字保证了线程安全

          • 数组拷贝:每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去。迭代过程中,不会影响到原来的数组,也不会抛出 ConcurrentModificationException 异常

          • volatile:一旦数组被修改,其它线程立马能够感知到

            在这里插入图片描述

      • 工作流程:从整体架构上来说,CopyOnWriteArrayList 数据结构和 ArrayList 是一致的,底层是个数组

        1. 加锁

        2. 从原数组中拷贝出新数组

        3. 在新数组上进行操作,并把新数组赋值给数组容器

        4. 解锁

          [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RhHML2I1-1659443487552)(C:\Users\10642\AppData\Roaming\Typora\typora-user-images\image-20220726221045973.png)]

          为什么使用CopyOnWriteArrayList而不使用Vector?

          Vector底层使用的是synchronized机制,效率很低;而CopyOnWriteArrayList使用的是lock锁机制

2. Set不安全

  • 同List一样,在多线程情况下,普通的Set集合是线程不安全的,也会出现ConcurrentModificationException并发修改异常。

  • 解决方案有:

    • 使用Collections工具类的synchronized包装的Set类
      • Set<String> set = Collections.synchronizedSet(new HashSet<>());
    • 使用CopyOnWriteArraySet 写入复制的JUC解决方案
      • Set<String> set = new CopyOnWriteArraySet<>();
    • HashSet的底层是什么?看源码:

    • 不难得出,HashSet的底层其实是HashMap,而HashSet的add方法底层其实就是HashMap里的put方法

    • PRESENT:

3. Map不安全

  • 在并发线程下,普通的HashMap是不安全的,也会出现ConcurrentModificationExecption并发修改异常

  • Map<String,Object> map = new HashMap<String,Object>(16,0.75f);

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ywt6ufPa-1659443487553)(C:\Users\10642\AppData\Roaming\Typora\typora-user-images\image-20220727091417646.png)]

  • 解决方法:

    • 使用Collections工具类的synchronized包装的Map类
      • Map<String,Object> map = Collections.synchronizedMap(new HashMap<>());
    • 使用ConcurrentHashMap
      • Map map = new ConcurrentHashMap<>();

3.1 ConcurrentHashMap

  • ConcurrentHashMap是线程安全的数组,是HashTable的替代品,同为线程安全,其性能要比HashTable更好。
JDK 7
  • 底层数据结构:Segments数组+HashEntry数组+链表,采用分段锁保证安全性,头插法

  • 实现原理:

    • 一个ConcurrentHashMap中有一个Segments数组,一个Segments中存储一个HashEntry数组,每个HashEntry是一个链表结构的元素。
    • segment继承自ReentrantLock锁。 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。
    • 可以通过构造函数指定,数组扩容不会影响其他的segment,get无需加锁,volatile保证内存可见性
  • get()操作:HashEntry中的value属性和next指针是用volatile修饰的,保证了可见性,所以每次获取的都是最新值,get过程不需要加锁。

    1. 将key传入get方法中,先根据key的hashcode的值找到对应的segment段。
    2. 再根据segment中的get方法再次hash,找到HashEntry数组中的位置。
    3. 最后在链表中根据hash值和equals方法进行查找。

    ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。

  • put()操作

    1. 将key传入put方法中,先根据key的hashcode的值找到对应的segment段
    2. 再根据segment中的put方法,加锁lock()
    3. 再次hash确定存放的hashEntry数组中的位置
    4. 在链表中根据hash值和equals方法进行比较,如果相同就直接覆盖,如果不同就插入在链表中。
JDK 8
  • 底层数据结构:Synchronized + CAS(compare and swap) +Node +红黑树。Node的val和next都用volatile保证,保证可见性,查找,替换,赋值操作都使用CAS。尾部插入

    为什么在有Synchronized 的情况下还要使用CAS?

    因为CAS是乐观锁,在一些场景中(并发不激烈的情况下)它比Synchronized和ReentrentLock的效率要高,当CAS保障不了线程安全的情况下(扩容或者hash冲突的情况下)转成Synchronized 来保证线程安全,大大提高了低并发下的性能.

  • 锁 : 锁是锁的链表的head的节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作(因为扩容的时候使用的是Synchronized锁,锁全表),并发扩容。

  • 读操作无锁 :

    • Node的val和next使用volatile修饰,读写线程对该变量互相可见
    • 数组用volatile修饰,保证扩容时被读线程感知
  • get()操作:get操作全程无锁。get操作可以无锁是由于Node元素的val和指针next是用volatile修饰的。在多线程环境下线程A修改节点的val或者新增节点的时候是对线程B可见的。

    1. 计算hash值,定位到Node数组中的位置
    2. 如果该位置为null,则直接返回null
    3. 如果该位置不为null,再判断该节点是红黑树节点还是链表节点
      • 如果是红黑树节点,使用红黑树的查找方式来进行查找
      • 如果是链表节点,遍历链表进行查找
  • put()操作

    1. 先判断Node数组有没有初始化,如果没有初始化先初始化initTable();
    2. 根据key的hashcode的值进行hash操作,找到Node数组中的位置,如果不存在hash冲突,即该位置是null,直接用CAS插入
    3. 如果存在hash冲突,就先对链表的头节点或者红黑树的头节点加synchronized锁
    4. 如果是链表,就遍历链表,如果key相同就执行覆盖操作,如果不同就将元素插入到链表的尾部, 并且在链表长度大于8, Node数组的长度超过64时,会将链表的转化为红黑树。
    5. 如果是红黑树,就按照红黑树的结构进行插入。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Daylan Du

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

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

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

打赏作者

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

抵扣说明:

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

余额充值