关于JUC的一些概述(三)

前面介绍了JUC的AQS原理、还有各种基于AQS的锁,如ReentrantLock、读写锁、信号量等关于JUC的一些概述(一)关于JUC的一些概述(二)。这一节主要介绍线程安全集合类

 线程安全集合类大概可以分承上述三大类

大概简单概述一下三大类

一.三种线程安全的集合类

1.遗留的安全集合

        我们都知道,在可以忽略线程安全问题的时候,我们推荐使用HashMap,而在需要保证线程安全时,我们可以使用HashTable,注意,这是可以而不是推荐,那么自然,没有推荐就意味着HashTable有自己的一个缺陷。主要的缺陷为:

  1. Hashtable使用了同步机制,避免了多个线程同时读写Hashtable。但在iterator遍历过程中其他线程对Hashtable的put、 remove、clear操作都会被成功执行。(弱一致性)
  2. HashTable性能很低,具体原因为其加锁的对象是table本身,锁的粒度太大。

        关于Vector就不做介绍了

2.使用Collections方法修饰的安全集合

在这个集合里,有很多线程安全的集合,如:

  • Collections.synchronizedCollection
  • Collections.synchronizedList
  • Collections.synchronizedMap
  • Collections.synchronizedSet
  • Collections.synchronizedNavigableMap
  • Collections.synchronizedNavigableSet
  • Collections.synchronizedSortedMap
  • Collections.synchronizedSortedSet

从方法名也可以看出,其大多数在内部都是使用了Synchronize进行了加锁,保证其操作的原子性。这里不做太多介绍

3.J.U.C安全集合

这里指的是java.util.concurrent.*下的线程安全集合类,其中它们大多包含了三类关键词:

Blocking、CopyOnWrite、Concurrent

  • Block大部分实现基于锁,并提供用来阻塞的方法。
  • CopyOnWrite 之类容器修改开销相对较重。
  • Concurrent 类型的容器
    • 内部很多操作使用 cas 优化,一般可以提供较高吞吐量。
    • 弱一致性
      • 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的。
      • 求大小弱一致性,size 操作未必是 100% 准确读取弱一致。

二.ConcurrentHashMap

在介绍ConcurrentHashMap前,我们先介绍一下HashMap存在的问题

1.HashMap存在的问题

        大家都知道,HashMap的数据结构是数组+链表的方式(JDK8后,链表可能会进化成红黑树),其维护了一个索引数组,通过对key的哈希值进行一定的算法可以得到其位于数组的中的哪个索引,再通过遍历当前索引下的链表,找到key符合的结点。

        但是在JDK7的时候,HashMap存在一个死链问题,这个问题会导致整个程序完全的卡死,即链表变成了循环链表。具体是为什么呢?

        在jdk7的情况下,HashMap的每次插入结点,都会插入到链表的头部,这时当多线程同时对HashMap进行操作时,可能会将其中一个链表产生死链。(具体实现的一个过程不在这赘述,感兴趣可以搜一下)。

        而在jdk8中,修复了这个问题,即每次插入结点时,都会插入到链表的尾部(为了保持与扩容前一样的顺序),但这仍不意味着能够在多线程环境下安全扩容,可能还会出现其他问题,如扩容丢数据。

2.JDK7的ConcurrentHashMap

        ConcurrentHashMap在JDK7和JDK8中的实现是不太相同的,这里将两个版本的实现都简单介绍一下。

 

        JDK7中,ConcurrentHashMap主要是由两个数组构成:Segment数组、HashEntry数组。其中Segment数组默认长度为16。这个容量初始化后就不能改变了,并且其并不是懒惰初始化的。

        其实JDK7下的ConcurrentHashMap有点类似二级HashMap,即在Segment数组下的每个segment,还会连接一个HashEntry数组

         在进行查找操作时,会先通过求出key的HashCode,然后通过散列算法找到其位于Segment数组的索引下标,然后再通过key代入某个函数,求出其位于HashEntry数组的下标,从而找到目标元素。

        在并发问题中,ConcurrentHashMap会将锁加到Segments数组的segment上,这样锁的粒度相对于Hashtable(加到了table上)小了很多,从而提高了性能。并且,多个线程执行不同的segment不会发生冲突。

3.JDK8的ConcurrentHashMap

         JDK8下的ConcurrentHashMap的结构和HashMap类似,不再像7那样存在一个Segments数组,而是直接维护一个table数组,并且对table数组实现了懒惰初始化,构造方法中仅计算了table的大小,但不会真正创建,而是在第一次使用时才会真正创建。

        且在jdk8中,还引入了链表升级红黑树的过程

        结构:数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)。

  • 初始化,使用 cas 来保证并发安全,懒惰初始化 table 。
  • 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程 会用 synchronized 锁住链表头
  • put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素添加至 bin的尾部
  • get,无锁操作仅需要保证可见性(volatile),扩容过程中 get 操作拿到的是 ForwardingNode(可以看作一个标志结点,如果它存在,则说明此时当前table的这个索引下的链表已经完成了迁移新table(扩容)的工作) 它会让 get 操作在新 table 进行搜索
  • 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容(帮忙扩容),扩容时平均只有 1/6 的节点会把复制到新 table 中
  • size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[](类似LongAddr原子累加器中的cell) 当中。最后统计数量时累加 即可

三.LinkedBlockingQueue

        LinkedBlockingQueue是一个线程安全的队列,它是一个底层基于链表的队列,当指定容量时,其为有界队列,否则时无界队列。

        它是一个典型的生产者-消费者模型,且其巧妙之处有两点:

        一是加了两把锁,分别位于队列的头部和尾部,这样可以保证取元素和存元素可以互不干扰(消费者与消费者串行,生产者与生产者串行),锁的类型是ReentrantLock,它通过使用条件变量Condition的等待通知来保证存取元素之间的通信。

        二是加了Dummy(哨兵)结点,这样可以保证链表中至少存在一个结点,并且不会出现链表中只有一个实际结点时,两把锁同时加到这个结点上。当链表中没有节点时,这时取元素的线程会被notEmpty条件阻塞,就会有竞争,不会和存元素的锁产生冲突。

线程安全分析

  • 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是 head 节点的线程安全。两把锁保证了入队和出队没有竞争
  • 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
  • 节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞

其删除结点是删除头结点 (Dummy),然后将第二个结点的item变成null(即将第二个节点变成Dummy) 

列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较

  • Linked 支持有界,Array 强制有界
  • Linked 实现是链表,Array 实现是数组
  • Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
  • Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
  • Linked 两把锁,Array 一把锁

四.CopyOnWriteArrayList

        CopyOnWriteArrayList底层实现采用了写入时拷贝的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其他线程的并发读,读写分离。适合读多写少的场景(有点类似缓存的作用),它只在新增操作上加了synchronize锁,其他操作均为加锁。因此其也具有弱一致性

不要觉得弱一致性就不好

  • 数据库的 MVCC 都是弱一致性的表现
  • 并发高和一致性是矛盾的,需要权衡

五.总结

        这里就把JUC下的线程安全集合给大概简述了一下,JUC的复习也到了一段落。需要注意的时,虽然这些集合时线程安全的,但只是保证了某个操作的线程安全(原子性),但如果有一系列的操作,还是会可能导致线程的不安全,如put线程安全,get线程安全,但两个组合时,若不保证其组合的原子性,还是会有线程不安全的现象发生。

        接下来会复习关于乐观锁以及悲观锁的一些底层细节。如乐观锁机制的CAS与volatile,悲观锁下的synchronize(ReentrantLock也是悲观锁,就不做赘述,但会有二者的比较)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值