1、并发容器
2、同步容器
为什么需要【并发容器】?
Hashtable 的实现基本就是将 put、get、size 等各种方法加上“synchronized”,这就导致了所有并发操作都要竞争同一把锁,一个线程在进行同步操作时,其他线程只能等待,大大降低了并发操作的效率。
来自《Java并发编程实战》:
【同步容器】(如 HashTable,Vector)将所有容器对象的访问都【串行化】,以实现它们的线程安全性。这种方法的代价是【严重降低并发性】,当多个线程竞争容器的锁时,吞吐量严重减低。
【同步包装器】只是利用输入 Map 构造了另一个同步版本,所有操作内部使用synchronized 语句块,利用【this】作为互斥的 mutex,没有真正意义上的改进!
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> {
private final Map<K,V> m;
// Object on which to synchronize
final Object mutex;
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);}
}
public boolean containsValue(Object value) {
synchronized (mutex) {return m.containsValue(value);}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
public V remove(Object key) {
synchronized (mutex) {return m.remove(key);}
}
}
3、ConccurentHashMap
3-1、JDK1.7之前 ConcurrentHashMap 实现
实现方法:
- 【分段锁】,也就是将内部进行【分段(Segment)】,里面则是 HashEntry 的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放。
- HashEntry 内部使用 【volatile】 的 value 字段来保证【可见性】,也利用了不可变对象的机制以改进利用 Unsafe 提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是 JVM intrinsic 优化过的。
- 在构造的时候,Segment 的数量由所谓的 concurrentcyLevel 决定,默认是 16,也可以在相应构造函数直接指定。注意,Java 需要它是 2 的幂数值,如果输入是类似 15 这种非幂值,会被自动调整到 16 之类 2 的幂数值。
- CocurrentHashMap也是基于【散列】的Hash;
- 迭代器不会抛出ConcurrentModificationException;ConccurentHashMap返回的迭代器具有弱一致性,可以容忍并发修改;
关于锁分段:
在某些情况下,可以将【锁分解技术】进一步扩展为对一组独立对象上的锁进行分解,这种情况称为锁分段。
ConcurrentHashMap的实现使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶有第(N mod 16)个锁来保护,使得ConcurrentHashMap能够支持多达16个并发的写入器;分段锁的数量可以增加。
锁分段的【劣势】在于:与采用【单个锁】来实现独占访问相比,要获取多个锁来实现独占访问将更加困难且开销更高。
get方法源码分析
public V get(Object key) {
// manually integrate access methods to reduce overhead
Segment<K,V> s;
HashEntry<K,V>[] tab;
// 两次Hash
int h = hash(key.hashCode());
//利用位操作替换普通数学运算
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// 以Segment为单位,进行定位
// 利用Unsafe直接进行volatile access
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u))
!= null && (tab = s.table) != null) {
//省略
}
return null;
}
put方法源码分析
通过【二次哈希】避免哈希冲突,然后以 Unsafe 调用方式,直接获取相应的 Segment,然后进行线程安全的 put 操作:
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
// 二次哈希,以保证数据的分散性,避免哈希冲突
int hash = hash(key.hashCode());
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>) UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
核心逻辑:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// scanAndLockForPut会去查找是否有key相同Node
// 无论如何,确保获取锁
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 更新已有value...
} else {
// 放置HashEntry到特定位置,如果超过阈值,进行rehash
// ...
}
}
} finally {
unlock();
}
return oldValue;
}
- ConcurrentHashMap 会获取再入锁,以保证数据一致性,Segment 本身就是基于 ReentrantLock 的扩展实现,所以,在并发修改期间,相应 Segment 是被锁定的。
- 在最初阶段,进行重复性的扫描,以确定相应 key 值是否已经在数组里面,进而决定是更新还是放置操作,你可以在代码里看到相应的注释。重复扫描、检测冲突是 ConcurrentHashMap 的常见技巧。
3-2、JDK1.8之后 ConcurrentHashMap 实现
- 总体结构上,它的内部存储变得和 HashMap 结构非常相似,同样是大的桶(bucket)数组,然后内部也是一个个所谓的链表结构(bin),同步的粒度要更细致一些。
- 其内部仍然有 Segment 定义,但仅仅是为了保证序列化时的兼容性而已,不再有任何结构上的用处。
- 因为不再使用 Segment,初始化操作大大简化,修改为 lazy-load 形式,这样可以有效避免初始开销,解决了老版本很多人抱怨的这一点。
- 数据存储利用 volatile 来保证可见性。
- 使用 CAS 等操作,在特定场景进行无锁并发操作。
- 使用 Unsafe、LongAdder 之类底层手段,进行极端情况的优化。
1、内部数据结构
Key 是 final 的,因为在生命周期中,一个条目的 Key 发生变化是不可能的;与此同时 val,则声明为 volatile,以保证可见性。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
2、put方法源码分析
优化点:
-
使用CAS获取对象;
-
使用Lazy-Load方式进行数组初始化;
/**
-
Table initialization and resizing control. When negative, the
-
table is being initialized or resized: -1 for initialization,
-
else -(1 + the number of active resizing threads). Otherwise,
-
when table is null, holds the initial table size to use upon
-
creation, or 0 for default. After initialization, holds the
-
next element count value upon which to resize the table. */ private transient volatile int sizeCtl;
/**
-
The default initial table capacity. Must be a power of 2
-
(i.e., at least 1) and at most MAXIMUM_CAPACITY. */ private static final int DEFAULT_CAPACITY = 16;
/**
-
The bin count threshold for using a tree rather than list for a
-
bin. Bins are converted to trees when adding an element to a
-
bin with at least this many nodes. The value must be greater
-
than 2, and should be at least 8 to mesh with assumptions in
-
tree removal about conversion back to plain bins upon
-
shrinkage. */ static final int TREEIFY_THRESHOLD = 8;
static final int HASH_BITS = 0x7fffffff;
public V put(K key, V value) { return putVal(key, value, false); }
static final int spread(int h) { // 使用高16位进行hash取值,因为越是高位的Hash值冲突较少 return (h ^ (h >>> 16)) & HASH_BITS; }
// CAS无锁操作 static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { // 对象,地址便宜量,新值,旧值 return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); }
// 使用 Unsafe 进行了优化,直接利用getObjectVolatile,避免间接调用的开销。 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { return (Node<K,V>) U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); }
/**
-
使用lazy-load的方式初始化数组 */ private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { // sizeCtl<0,说明在初始化或者调整大小,如果发现冲突,进行spin等待 // sizeCtl用volatile修饰,保证并发情况下可见性 if ((sc = sizeCtl) < 0) Thread.yield(); else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // CAS成功返回true,则进入真正的初始化逻辑 try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) // key 或 value 为 null 抛出NPE; throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 利用CAS去进行无锁线程安全操作 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break;
} else if ((fh = f.hash) == MOVED) // 需要移动时进行转移 tab = helpTransfer(tab, f); else { V oldVal = null; // 细粒度加锁 synchronized (f) { // CAS加锁操作 if (tabAt(tab, i) == f) { // ...省略操作 } } if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }/**
-
Helps transfer if a resize is in progress. */ final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { Node<K,V>[] nextTab; int sc; if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { int rs = resizeStamp(tab.length); while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) { if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0) break; if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { transfer(tab, nextTab); break; } } return nextTab; } return
-
3-3、为什么ConcurrentHashMap和HashTable的key、value不能为null
因为HashMap是非线程安全的,默认单线程环境中使用,如果get(key)为null,可以通过containsKey(key) 方法来判断这个key的value为null,还是不存在这个key,
而ConcurrentHashMap,HashTable是线程安全的, 在多线程操作时,因为get(key)、containsKey(key) 两个操作合在一起不是一个原子性操作, 可能在执行中间,有其他线程修改了数据,所以无法区分value的值为null还是不存在key。containsValue也是如此。
4、CopyOnWriteArrayList
4-1、什么是写入时复制(Copy-On-Write)?
- 【写入时复制(Copy-On-Write)】容器的线程安全性在于,只要正确地发布一个事实不可变的对象,那么访问该对象时就不再需要进一步的同步。
- 在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性;
- 写入时复制,容器的迭代器保留一个指向底层基础数组的引用,这个数组当前位于迭代器的起始位置,由于它不会被修改,因此对进行同步时,只需要确保数组内容的可见性(用volatile表示);
- 因此,多线程可以同时对这个容器进行迭代,不会彼此干扰或者与修改容器的线程相互干扰;
- 【写入时复制】容器,返回的迭代器,不会抛出ConcurrentModificationException,返回的元素与迭代器创建时的元素完全一致,不必考虑之后修改操作带来的影响;
- 每当修改时,都会复制底层数组,会造成一定开销,因此仅当【迭代操作远远多于修改操作】时,才应该使用【写入时复制】容器,也就是【读多于写】的场景;
CopyOnWriteArrayList是一个线程安全的ArrayList,其中使用 ReentrantLock作为锁保证线程安全;
CopyOnWriteArrayList新增元素时,会将 array 复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将 array 指向这个新的数组。
getArray() 赋给 elements,在复制出来的数组上进行添加,然后再重新赋值;
CopyOnWriteArrayList 仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致;
写入时,同时先复制数组,写入完成后,再给数组赋值,使用volatile修饰数组,保证多线程情况下的可见性;