如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全?
如何保证线程安全:
-
早期的同步容器及同步包装器:
- 早期同步容器:Vector、HashTable等;
- 同步包装器:通过Collections.synchronizedMap()等方法获取;
- 不足:他们都是采用非常粗粒度的同步方式,在高并发场景下,效率比较低下;更加普遍的选择是利用并发包提供的线程安全类容器
- 并发包提供的线程安全容器:
- 如:ConcurrentHashMap、CopyOnWriteArrayList以及各种线程安全的队列:ArrayBlockingQueue、SynchronousdQueue等等
- 并发包提供的线程安全容器在高并发场景下性能远优于早期的同步容器;
ConcurrentHashMap如何实现高效的线程安全(JDK8U201)
ConcurrentHashMap除了在同步上做了工作外,部分细节做了一些调整:
-
get函数对哈希码获取的改动:
static final int spread(int h) { return (h ^ (h >>> 16)) & HASH_BITS; }
HashMap在get函数中会对key进行非空判断,并将key.hashCode右移16位后疑惑上自身来获得哈希码,ConcurrentHashMap,则不会对key进行非空判断,如果key为null,则抛出空指针异常,猜测是为了极致性能做的改动,另外哈希码也做了改动,屏蔽掉了符号位(HASH_BITS为0x7fffffff);
查询时候如果首元素哈希码一致,检查首元素key 是不是就是key 的时候的equals检测也从key != null && key.equals(ek)改为了ek != null && key.equals(ek); 其中key为查询对象,ek为首元素的key(因为程序设计者不可能大量的使用null去查询,但是有可能插入了null的key,这样在判断时效率会高一点);
ConcurrentHashMap的put函数:
-
put仅仅是调用了putVal函数:
public V put(K key, V value) { return putVal(key, value, false); }
-
putVal:
-
第一句就是:
if (key == null || value == null) throw new NullPointerException();
由此可以看出,ConcurrentHashMap是不允许插入null键和值的;
当桶为null时,使用CAS进行放置,当桶不为null时,使用sychronized关键字锁定首元素进行操作;
-
-
initTable函数:
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { 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; }
双重检查加锁机制,保证table只会被赋值一次;
并发容器有哪些:
List
- CopyOnWriteArrayList
Set
- CopyOnWriteArraySet
- ConcurrentSkipListSet
Map
- ConcurrentHashMap
- ConcurrentSkipListMap
队列
- ConcurrentLinkedQueue
- ConcurrentLinkedDeque
阻塞队列
- ArrayBlockingQueue
- LinkedBlockingQueue
- PriorityBlockingQueue
- DelayQueue
- SynchronousQueue
- LinkedTransferQueue
- LinkedBlockingDequeue
CAS操作的ABA问题
ABA问题指得是使用compareAndSwap之类的函数对变量进行无锁并发操作,当变量为预期的某一个值时,就进行更新,否则不更新;改过程中A可能已经变为B,然后又变回了A,而我们只看到他是A就执行力更新操作,而忽略了它曾经变化过;
- 对于网上很多人说的ABA的危害的例子并不能算作ABA问题,而是那些例子本来就不适合CAS操作来解决;如:往链表末尾插入元素,期间链表发生了变化;
- 看到某网友比较形象的比喻:我们看到桌子上有一杯纯净水,我们打算将其替换为矿泉水,但当我准备矿泉水的时候,别人已经将他喝掉了,杯子空了,然后他又使用自来水将杯子灌满,我们看到以为还是纯净水,就用矿泉水将其替换,但我们的本意是,如果杯子里是纯净水,我们才会去替换,这个类比的例子才真正解释了ABA问题的所在;
- 对于基本类型,ABA问题影响并不大,一般无需关心,对于引用类型,CAS比较的是地址,ABA问题应该描述的是,地址还是那个地址,但该地址存放的内容已不是我们预期的内容;因为需要比较,所以我们一定会持有旧的引用,所以GC应该不会碰这种对象,即便是JVM进行引用修正,但对于上层应该是透明的,所以目前我还无法举出实际例子来说明这种情况,也不确定在现代JVM中是否会有这样的情况发生;