一、并发容器概览
ConcurrentHashMap: 线程安全的HashMap
CopyOnWriteArrayList:线程安全的ArrayList
BlockingQueue:是一个接口,表示阻塞队列,非常适合用于作为数据共享的通道。
ConcurrentLinkedQueue:高效的非阻塞并发队列,使用链表实现。可看作一个线程安全的LinkedList。
ConcurrentSkipListMap:是一个Map,使用跳表的数据结构进行快速查找。
二、ConcurrentHashMap
1.Map简介
Map的实现:
2.为什么HashMap不安全
- 同时put碰撞导致数据丢失;
- 同时put扩容导致数据丢失;
- 会形成死循环:JDK1.7中,扩容采取头插法,而且通过重新计算哈希值来计算新的地址。这样在扩容时就可能会形成环。
3.ConcurrentHashMap分析
(1)JDK1.7中
1.7中的ConcurrentHashMap最外层是多个segment,每个segment的底层数据结构与HashMap类似,仍然是数组和链表组成的拉链法。
为每个segment独立上ReentrantLock锁,每个segment之间互不影响,提高了并发效率。
默认有16个segment,所以最多有16个线程并发写。这个默认值可以在初始化时指定其他值,但是初始化之后就不可扩容了。
(2)JDK1.8中
不使用segment,而是node。使用synchronized+CAS保证并发安全。
结构与1.8的HashMap类似。
putVal()源码分析
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
从这一段代码可以看到,如果通过哈希值判断对应下标位置是空的,就用casTabAt
方法将其插入。而casTabAt用的正是CAS的方法。
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
这一段是向链表中插入数据的代码,可以看到使用synchronized包裹来保证并发安全。
(3)为什么7到8要修改结构
- 提高了并发性
- 链表 --> 链表/红黑树,效率更高
三、CopyOnWriteArrayList
1.简介
CopyOnWriteArrayList是线程安全的ArrayList,用来代替Vector和SynchronizedList。
使用场景:
- 读操作可以尽可能地快,而写即使慢一些也没有太大关系。(商品列表、黑名单)
CopyOnWriteArrayList的读写规则:
- 读取完全不加锁
- 写入时不会阻塞读取操作(和读写锁不同)。只有写入和写入之间需要同步。
CopyOnWriteArrayList可以在迭代过程中修改,ArrayList不能在迭代过程中修改。尽管可以修改,但是在迭代过程中读取的还是修改之前的内容。
2.实现原理
在写的时候,复制一份出来。修改复制出来的那部分,修改完毕后将原来的引用指向新的部分。
所以,新旧互不干涉,在读取的时候还是读的旧的部分。
3.缺点
- 数据一致性问题:只能保证数据的最终一致性,不能保证数据的实时一致性。
- 内存占用问题:进行写操作时,内存中会同时驻扎两个对象内存。
4.源码分析
- 和ArrayList一样,底层采用数组实现。
- 保证线程安全的方式是ReentrantLock
四、并发队列
1.为什么用
使用队列就可以在线程间传递数据:如生产者消费者问题;
如果队列是并发安全的,那么直接就可以用了,而不用去考虑线程安全问题。
2.关系
3.阻塞队列★
(1)定义
阻塞队列是具有阻塞功能的队列。通常阻塞队列一端是给生产者放数据用,另一端消费者拿数据用。阻塞队列是线程安全的。
(2)方法
- take():取数据,队列为空时阻塞
- put():放数据,队列满时阻塞
- add():放数据,如果满了会抛出异常
- remove():取数据,空了会抛出异常
- element():查看头部,空了会抛出异常
- offer():放数据,满了返回false
- poll():取数据,为空会返回null
- peek():查看队首,为空返回null
(3)实现
ArrayBlockingQueue
有界,可以指定公平还是不公平。
使用数组实现,用ReentrantLock保证线程安全。
LinkedBlockingQueue
是无界的。
使用链表作为底层数据结构。Node存放数据,有两把ReentrantLock(put、take)。
PriorityBlockingQueue
线程安全的PriorityQueue,也是无界的。
SynchronizedQueue
容量为0,直接交换元素,不存储
DelayQueue
延迟队列,根据延迟时间排序。所有元素必须实现Delayed接口。
4.非阻塞队列
只有ConcurrentLinkedQueue这一种实现,其使用CAS来实现线程安全。