1. ConcurrentHashMap
源码原理分析
2.1 HashMap简介
- HashMap是一个线程不安全的类,不能在多线程下使用
- JDK1.7结构:数组+链表(采用拉链法)
- JDK1.8结构:数组+链表/红黑树(链表长度要大于阈值8)
2.1 JDK1.7的ConcurrentHashMap
的实现
-
- 1.7结构可以看这篇博客:ConcurrentHashMap1.7
- 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,有就取出来给你
3. 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<String> list = new ArrayList<>();
CopyOnWriteArrayList<String> 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的迭代器不一样,ArrList里面有一个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<String> 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方法
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//添加的时候先上锁
lock.lock();
try {
// copy一份到新数组,数组长度+1
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 新值放到末尾,把指针指过去
newElements[len] = e;
// 最后返回true 并释放锁
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);
}