并发编程学习7—源码理解ConcurrentHashMap及其他线程安全集合类

线程安全集合类可以分为三大类:

  1. 遗留的线程安全集合如 Hashtable , Vector

  2. 使用 Collections 装饰的线程安全集合,如:
    Collections.synchronizedCollection
    Collections.synchronizedList
    Collections.synchronizedMap
    Collections.synchronizedSet
    Collections.synchronizedNavigableMap
    Collections.synchronizedNavigableSet
    Collections.synchronizedSortedMap
    Collections.synchronizedSortedSet
    java.util.concurrent.*

  3. 重点介绍 java.util.concurrent.下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:Blocking、CopyOnWrite、Concurrent。

Blocking 大部分实现基于锁,并提供用来阻塞的方法。
CopyOnWrite 之类容器修改开销相对较重。
Concurrent 类型的容器。

  1. 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
  2. 弱一致性
    -遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
    -求大小弱一致性,size 操作未必是 100%
    -准确读取弱一致性

1. ConcurrentHashMap

1. 1 练习:单词计数

在这里插入图片描述

1. 2 问题与解决

虽然用了ConcurrentHM,但get个put两个是不能保证原子性的。
所以用里面包装在一个方法里的,computeIfAbsent() (里面把synchronized加在一个链表头上)。
上面改成Integer value = map.computeIfAbsent(word, (key)->1);
但此时只能给1,不能累加。这时可以用LongAdder。
在这里插入图片描述

2. HashMap原理

HashMap不完全的实现有哪些并发问题。
拉链法:jdk7头插,8尾插。 链表长度到原来数组的3/4阈值时,进行扩容。 容量翻倍,重新算下标。

2. 1 JDK 7 HashMap 并发死链

P279
在这里插入图片描述

死链,究其原因,是因为在多线程环境下使用了非线程安全的 map 集合。

JDK 8 虽然将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),但仍不意味着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)。

2. 2 JDK 8 ConcurrentHashMap

2. 2. 1 重要属性和内部类

在这里插入图片描述

2. 2. 2 重要方法

在这里插入图片描述

2. 2. 3 构造器分析

在这里插入图片描述

2. 2. 4 get 流程

在这里插入图片描述

  1. 根据 hash 值计算位置。
  2. 查找到指定位置,如果头节点就是要找的,直接返回它的 value.
  3. 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找它。如果是链表,遍历查找。
2. 2. 5 put 流程
  1. 根据 key 计算出 hashcode 。
  2. 判断是否需要进行初始化。即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  3. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  4. 如果都不满足,则利用 synchronized 锁写入数据。
  5. 如果数量大于 TREEIFY_THRESHOLD 则要执行树化方法,在 treeifyBin 中会首先判断当前数组长度 ≥64 时才会将链表转换为红黑树。
2. 2. 6 size 计算流程

size 计算实际发生在 put,remove 改变集合元素的操作之中

  1. 没有竞争发生,向 baseCount 累加计数
  2. 有竞争发生,新建 counterCells,向其中的一个 cell 累加计数
  counterCells 初始有两个 cell
    如果计数竞争比较激烈,会创建新的 cell 来累加计数

Java 8 数组(Node) +(链表 Node | 红黑树 TreeNode )以下数组简称(table),链表简称(bin)

  1. 初始化,使用 cas 来保证并发安全,懒惰初始化 table

  2. 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程会用 synchronized 锁住链表头

  3. put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素添加至 bin 的尾部

  4. get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 它会让 get 操作在新table 进行搜索

  5. 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容,扩容时平均只有 1/6 的节点会把复制到新 table 中

  6. size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加即可

2. 3 JDK 7 ConcurrentHashMap

比较:
Java7 中 ConcurrentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。

Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构从 Java7 中的【 Segment 数组 + HashEntry 数组 + 链表】 进化成了 【Node 数组 + 链表 / 红黑树】,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。

优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的
缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化

3. LinkedBlockingQueue

3. 1 基本的入队出队

在这里插入图片描述

入队:
在这里插入图片描述

出队:
在这里插入图片描述

3. 2 加锁分析

# LinkedBlockingQueue的高明之处在于用了两把锁和 dummy 节点

  1. 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
  2. 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
    消费者与消费者线程仍然串行
    生产者与生产者线程仍然串行

线程安全分析:

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

3. 3 性能比较

主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较:

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

4. ConcurrentLinkedQueue 原理

ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是

  1. 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行。
  2. dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争。
  3. 只是这【锁】使用了 cas 来实现,所以这是一个非阻塞队列。

拓展:
ConcurrentLinkedQueue 应用还是非常广泛的。
比如之前的 Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,采用了ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用。

5. CopyOnWriteArrayList

实现线程安全的List
ReentrantReadWriteLock 读写锁的设计很类似,只有读读不互斥。
CopyOnWriteArrayList 中的读取操作是完全无需加锁的。写入操作也不会阻塞读取操作,只有写写才会互斥。这样一来,读操作的性能就可以大幅度提升。

底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份副本,更改操作在副本数组上执行,修改完之后再将修改后的数组赋值回去,这样就不影响其它线程的并发读和写,读写分离。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值