• 多线程(进阶四:线程安全的集合类)


    目录

    一、多线程环境使用ArrayList

    二、多线程环境使用队列

    三、多线程环境使用哈希表

    1、HashMap

    2、Hashtable

    3、ConcurrentHashMap

    (1)缩小了锁的粒度

    (2)充分使用了CAS原子操作,减少一些加锁

    (3)针对扩容操作的一些优化(化整为零)

    四、相关面试题


    大部分集合类都是线程不安全的,Vector,Stack,Hashtable是线程安全的,但不建议使用,因为无论什么情况都要加锁,甚至单线程也是,这样就很不合理;并且这几个集合类官方已经不推荐使用了,可能在未来的版本中就被删掉了。

    下面介绍一些线程不安全的集合类。

    一、多线程环境使用ArrayList

    1、自己使用同步机制(synchronized或者ReentrantLock)

    2、Collections.synchronizedList(new ArrayList);

            相当于给ArrayList套了个壳,ArrayList各种操作本身是不带锁的,通过上述操作套壳后,得到了新的对象,新的对象里面的关键方法都是带有锁的。

    3、使用CopyOnWriteArrayList

            CopyOnWrite容器即写时复制的容器,多线程对这个顺序表进行读操作时,不会有线程安全问题,但是当多线程进行写操作时,就会有线程安全问题,CopyOnWriteArrayList会复制一份原来的顺序表,并且修改新的顺序表内容,再把原来的引用指向新的顺序表(此操作是原子的,不需要加锁)。


    二、多线程环境使用队列

    1、自己加锁

    2、使用BlockingQueue

    1. ArrayBlockingQueue
    基于数组实现的阻塞队列
    2. LinkedBlockingQueue
    基于链表实现的阻塞队列
    3. PriorityBlockingQueue
    基于堆实现的带优先级的阻塞队列
    4. TransferQueue
    最多只包含⼀个元素的阻塞队列


    三、多线程环境使用哈希表

    1、HashMap

            HashMap本身就是线程不安全的。

    2、Hashtable

            在一些关键方法上加了锁

            这也相当于对this加了锁,也就是针对Hashtable对象本身加锁,如果尝试修改Hash表中两个不同Hash值里的链表,会发生锁冲突。如图:

    3、ConcurrentHashMap

    相对于Hashtable,进行了些优化。

    (1)缩小了锁的粒度

            多线程如果修改Hash表里Hash值不同的链表都发生锁冲突,是不合理的,而且锁冲突是很耗时的,所以ConcurrentHashMap是对Hash表里每个链表都进行加锁,这样,不同的链表有不同的锁对象,多线程修改两个不同的链表,就不会发生锁冲突了,如图:

    注意:更多的锁并不意味着要耗费更多的空间,因为在java中的任何对象都可以作为锁对象,而本身Hash表中就得有数组,数组元素都已经存在,即链表的头结点,每个链表都有一个头结点,可以直接把这个头结点作为链表的锁对象。

    (2)充分使用了CAS原子操作,减少一些加锁

            比如,针对Hash表元素个数的维护。

    (3)针对扩容操作的一些优化(化整为零)

            负载因子:描述了每个桶(Hash表)平均有多少个元素;公式:实际个数 / 数组长度(桶的个数)。0.75是默认的扩容阈值(也可以是其他数字值),如果我们算出的负载因子超过规定的扩容阈值,Hash表就会进行扩容。

            进行扩容时,如果不是concurrentHashMap,会创建一个更大是数组,把旧的数组元素搬运到新的数组中,一次性的全部搬运完,如果Hash表本身的元素就非常多,这里扩容就会非常耗时,但可能过一会儿就又好了,存在不稳定因素,我们无法控制Hash表何时触发扩容。

            concurrentHashMap则不是一次性的全部搬运完,而是把Hash表中的元素分为若干次搬运完,而不是直接一次性梭哈完,假设Hash表有1kw个元素,每次就只搬运5k哥元素,一共花费2k次搬运完成(搬运的时间会更长一些),但能确保每次搬运消耗的时间不会很长,避免出现很卡的情况。

    总的来说,扩容是一个低频的操作(前提把扩容阈值设置合理),运行整个程序,可能一天都不会触发扩容,触发了每次可能会花费几分钟的时间进行搬运。

    注意:在扩容过程中,存在两份Hash表,一份是新的,一份是旧的。

            进行插入操作,直接往新的Hash表上插入。

            进行删除操作,新的旧的都要删除。

            进行查找操作,新的旧的都要查找。


    四、相关面试题

    1.ConcurrentHashMap的读是否要加锁,为什么?

     读操作没有加锁.目的是为了进一步降低锁冲突的概率.为了保证读到刚修改的数据,搭配了volatile关键字.

    2.介绍下ConcurrentHashMap的锁分段技术?

    这个是Java1.7所采取的技术.Java1.8中已经不再使用了.简单的说就是把若干个哈希桶分成一个"段"(Segment),针对每个段分别加锁.

    目的也是为了降低锁冲突的概率.当两个线程访问的数据恰好在同一个段上时,才会触发锁竞争

    3.ConcurrentHashMap在jdk1.8做了哪些优化?

    取消了分段锁,直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头节点对象作为锁对象).

    将原来的数组 + 链表的实现方式改进成 数组 + 链表 /红黑树的方式.当链表较长的时候(大于等于8个元素)就转换成红黑树. 

    4.HashMap和HashTable,ConcurrentHashMap之间的区别?

    HashMap: 线程不安全.key允许为null

    HashTable:线程安全.使用synchronized锁HashTable对象,效率较低.key不允许设置为null.

    ConcurrentHashMap: 线程安全.使用synchronized锁每个链表的头节点,锁冲突概率较低,充分利用CAS机制,优化了扩容方式.key不允许为null. 


    都看到这了,点个赞再走吧,谢谢谢谢谢

  • 相关阅读:
    【论文阅读】BGE Landmark Embedding: 一种用于大语言模型长上下文检索增强的嵌入方法
    AI视频剪辑:批量智剪技巧大揭秘
    1260. 二维网格迁移 : 简单构造模拟题
    基于javaweb高校浴池管理系统
    idea plugins一直在转圈解决方法
    双碳管理系统任务需求分析(第10套)
    vue+Ts+element组件封装
    【WordPress】在 Ubuntu 系统上使用 Caddy 服务器来发布 WordPress 网站
    Java资深架构师详解java进阶技术体系与主流架构思维(建议入手)
    HTTP协议 学习笔记
  • 原文地址:https://blog.csdn.net/cool_tao6/article/details/136018410