文章主要部分分析了两个并发容器的代表:ConcurrenthashMap和CopyOnWriteArrayList
- 并发容器
1.1 常用的并发容器
ConcurrentHashMap:线程安全的HashMap
CopyOnWriteArrayList:线程安全的List
BlockingQueue:阻塞队列(实现类:ArrayBlockingQueue、LinkedBlockingQueue等)
ConcurrentLinkedQueue:非阻塞并发队列,链表实现,可看做线程安全的LinkedList
ConcurrentSkipListMap:使用跳跃链表实现的Map
1.2 早期的并发集合
Vector和HashTable
内部方法全部用synchronized做同步,并发性能差得一批!
HashMap和ArrayList
虽然这两个类不是线程安全的,但是可以用Collections的synchronizedList(list)和synchronizedMap(map)包装使之变成线程安全的
但是实质上,里面还是用synchronized实现, 以map为例:看源码:
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
复制代码
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
…
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;
}
…
public int size() {
synchronized (mutex) {return m.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return m.isEmpty();}
}
public boolean containsKey(Object key) {
synchronized (mutex) {return m.containsKey(key);}
}
…
复制代码
我们可以看到源码中,虽然synchronized不是加在方法上,但是其实每个方法都是在调用对应map的方法并在外层包裹synchronized
1.3 现在的并发集合
ConcurrenthashMap和CopyOnWriteArrayList
用于取代同步的HashMap和ArrayList以及HashTable和Vector这些容器
多数条件下,性能优于以前的容器。
注:CopyOnWriteArrayList适合于读多写少的场景,每次写入都会复制整个列表,而原有的比较适合频繁修改
- ConcurrentHashMap源码原理分析
2.1 关于HashMap
HashMap是一个线程不安全的类也不能在多线程下使用
并发下的问题:
同时put时如果hash碰撞,会丢失数据
同时put触发扩容的时候也会导致数据丢失
死循环造成CPU100%(JDK1.7及以前版本多线程同时触发扩容可能引发)
问题详解:coolshell.cn/articles/96…
JDK1.7结构:数组+链表(采用拉链法)
JDK1.8结构:数组+链表/红黑树(链表长度要大于阈值8,还要满足 数组的长度table.length>=MIN_TREEIFY_CAPACITY 这个值是64)
2.1 JDK1.7的ConcurrentHashMap的实现
JDK7中,ConcurrentHashMap最外层是多个segment,每个segment的底层数据结构与HashMap类似,任然是数组+链表组成的拉链法
每个Segment独立上ReentrantLock锁,每个Segment之间互不影响,提高了并发效率(Segment继承自ReentrantLock)
ConcurrentHashMap默认有16个segment,所以最多支持16个线程并发写(操作在不同的segment上时)。默认值在初始化的时候可以指定,但是一旦初始化过后,就不可以扩容。但是每个Segment内部是可以扩容的
2.2 JDK1.8的ConcurrentHashMap的源码分析
根本没有借鉴JDK1.7,而是重写了一遍。。。
JDK1.8中的ConcurrentHashMap结构和1.8中的HashMap结构是相似的,也是数组+链表/红黑树(阈值也是8不过还要满足table.length>=MIN_TREEIFY_CAPACITY 这个值是64)
put方法
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key-value的值不能为空
if (key == null || value == null) throw new NullPointerException();
// 计算hash
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// table如果为空,或者长度为零就执行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 找出节点需要放置的位置如果为空,然后用CAS来赋值
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
}
// 如果处于MOVED状态 就帮助转换
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 如果table上要放的位置不为空就执行下列操作
else {
V oldVal = null;
// 锁住当前table上的位置
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//key相同就替换
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;
}
}
}
}
// 检查是否满足阈值
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 满足时就把链表转成红黑树
// 注意此方法里面还有一个判断tab.length小于64的不转化
treeifyBin(tab, i);
// 如果老值不为空就返回
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
复制代码
get方法
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算hash值
int h = spread(key.hashCode());
// 排除为空的情况,并找到对应位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果相等就直接在table上取值
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 在红黑树中找值
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 在链表中找值
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
复制代码2.3 对比JDK1.7与1.8
首先是数据结构上:
1.7是segment数组,+Segment(类似HashMap的结构)
1.8是数据+链表/红黑树与HashMap类似
并发上:
1.7是使用ReentrantLock锁住每个Segment
1.8是使用CAS+synchronized
为什么超过8要使用红黑树
首先链表的结构存储要比红黑树存储节省空间
而链表在查询上又没有红黑树块
这个时候就需要一个边界,源码作者做了一个泊松分布运算,在链表达到8时的概率已经非常小了。而链表长度为8时,查找费时也不大。概率只有千万分之几
2.4 线程安全问题
ConcurrenthashMap并发下单独操作的确是安全的,但是组合操作就未必了。所以如果在多线程情况下,有多步操作ConcurrenthashMap的时候需要额外留心
如:如果要修改一个值:可以使用boolean replace(key, oldValue, newValue)来修改,而不是先get然后put, 这个方法类似于CAS的思想
此外还有putIfAbsent(key, value) ,先判断有没有这个值,如果没有就put,有就取出来给你
- CopyOnWriteArrayList源码原理分析
3.1 使用场景
是用于替代Vector和SynchronizedList的,相较于Vector和SynchronizedList有更好的并发性能
Copy-on-Write并发容器还包括CopyOnWriteArraySet,用来替代同步Set
主要适用于:对于读操作有快速要求的,即是:读快写慢
3.2 读写规则
我们都知道读写锁的规则是:读写互斥,写写互斥
而CopyOnWrite则做了一个升级:读取是完全不加锁的,并且写入也不会阻塞读取操作,只有写入和写入之间需要进行同步等待。
此外,我们可以在迭代中可以进行删改元素,看一个案例
/**
-
CopyOnWriteArrayList可以在迭代中修改数组内容,而ArrayList不行
-
@author yiren
*/
public class CopyOnWriteArrayListExample01 {
public static void main(String[] args) {
// ArrayList list = new ArrayList<>();
CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();list.add("1"); list.add("2"); list.add("3"); list.add("4"); list.add("5"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { System.out.println(list); String next = iterator.next(); System.out.println(next); if (next.equals("2")) { list.remove("3"); } if (next.equals("4")) { list.add("3 add"); } }
}
}
复制代码[1, 2, 3, 4, 5]
1
[1, 2, 3, 4, 5]
2
[1, 2, 4, 5]
3
[1, 2, 4, 5]
4
[1, 2, 4, 5, 3 add]
5
Process finished with exit code 0
复制代码
我们可以看到结果输出。这似乎和list中的元素不对应啊。
其实这时没毛病的,CopyOnWriteArrayList就是这个思想,迭代你虽然可以改,但是你改你的,我迭代我的,它内部是有副本机制的
而ArrayList的迭代器是有一个modCount值来判断你迭代过程中是否修改的
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
复制代码
这个expectedModCount是在迭代器创建前,从ArrayList对象中获取的,原有ArrayList对象左右删改,那么modCount就会和expectedModCount不一值,此时就会快速失败了。
3.3 实现原理
CopyOnWrite:在写入操作的时候,它会先copy一份到新内存上,然后再修改,修改完成,再把原来的指针指过去,就OK。
这个过程就导致了,你在迭代的时候,迭代的内存还是老内存上的值,而不是修改过后的值
所以注意:每次修改或添加都会创建新副本,使之读写分离,而旧的内存数据是不会变的。
我们再看一个案例
/**
-
@author yiren
*/
public class CopyOnWriteArrayListExample02 {
public static void main(String[] args) {
CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();list.add("1"); list.add("2"); list.add("3"); Iterator<String> itr1 = list.iterator(); list.add("4"); Iterator<String> itr2 = list.iterator(); itr1.forEachRemaining(System.out::print); System.out.println(); itr2.forEachRemaining(System.out::print);
}
}
复制代码123
1234
Process finished with exit code 0
复制代码
在CopyOnWrite的迭代器使用上,即使你修改了,它的迭代内容也只取决于他创建时候的集合的数据内容。而不取决于实际list是否修改。
所以迭代过程可能会出现数据过期问题
3.4 存在的缺点
数据一致性问题:也就是上面所提到的,它只能保证最终数据一致性,而不保证数据实时一致性。如果对写入实时响应的需求,不推荐使用。
内存浪费:CopyOnWrite的写是复制的机制,写操作的时候就一定会复制一份。这会很浪费内存
3.5 源码分析
首先CopyOnWriteArrayList是一个数组的列表集合,它的根本存储就是数组
private transient volatile Object[] array;
复制代码
多线程同时写入的时候是ReentrantLock。
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
复制代码
它的创建,构造函数可想而知,也就是给一个空数组。
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
复制代码
但是它提供了一个可以直接放集合的构造函数,把数据先放入数组,然后直接指过去
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
复制代码
add方法
添加的时候先上锁
然后先获取到当前数组,copy一份到新数组,数组长度+1
然后把新值放到末尾,把指针指过去
最后返回true 并释放锁
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
复制代码
get方法
没有任何加锁,直接返回对应的值
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}