Java并发编程—并发容器
HashMap是线程不安全的
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//modCount++ 是一个复合操作
modCount++;
addEntry(hash, key, value, i);
return null;
}
从表面上看 i++ 只是一行代码,但实际上它并不是一个原子操作。并且除此之外可能会出现以下几点线程安全问题
1)扩容期间取出的数据可能不准确。可能取出空值。
2)同时put碰撞导致数据丢失。碰撞数据都保存到同一位置,导致丢失。
3)可见性无法保证。
4)死循环造成CPU 100%。JDK1.8之前的头插法会导致死循环,但1.8之后改成了尾插法。
ConcurrentHashMap
JDK1.7
在 ConcurrentHashMap 内部进行了 Segment 分段,Segment 继承了 ReentrantLock,可以理解为一把锁,各个 Segment 之间都是相互独立上锁的,互不影响。相比于之前的 Hashtable 每次操作都需要把整个对象锁住而言,大大提高了并发效率。因为它的锁与锁之间是独立的,而不是整个对象只有一把锁。
每个 Segment 的底层数据结构与 HashMap 类似,仍然是数组和链表组成的拉链法结构。默认有 0~15 共 16 个 Segment,所以最多可以同时支持 16 个线程并发操作(操作分别分布在不同的 Segment 上)。16 这个默认值可以在初始化的时候设置为其他值,但是一旦确认初始化以后,是不可以扩容的。
JDK1.8
Java 7 采用 Segment 分段锁来实现,而 Java 8 中的 ConcurrentHashMap 使用数组 + 链表 + 红黑树,在这一点上它们的差别非常大。锁粒度更细,理想情况下 table 数组元素的个数(也就是数组长度)就是其支持并发的最大个数,并发度比之前有提高。采用 Node + CAS + synchronized 保证线程安全。
CopyOnWriteArrayList
适用场景
1)读操作可以尽可能的快,而写即使慢一些也没关系。
2)读多写少
读写规则
1)读写锁的思想:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)
2)读写升级:CopyOnWriteArrayList 读取是完全不用加锁的,更厉害的是,写入也不会阻塞读取操作,也就是说你可以在写入的同时进行读取,只有写入和写入之间需要进行同步
特点
从 CopyOnWriteArrayList 的名字就能看出它是满足 CopyOnWrite 的 ArrayList,CopyOnWrite 的意思是说,当容器需要被修改的时候,不直接修改当前容器,而是先将当前容器进行 Copy,复制出一个新的容器,然后修改新的容器,完成修改之后,再将原容器的引用指向新的容器。这样就完成了整个修改过程。
迭代修改
ArrayList 在迭代期间如果修改集合的内容,会抛出 ConcurrentModificationException 异常。原因是,在 ArrayList 源码里的 ListItr 的 next 方法中有一个 checkForComodification 方法。
和 ArrayList 不同的是,CopyOnWriteArrayList 的迭代器在迭代的时候,如果数组内容被修改了,CopyOnWriteArrayList 不会报 ConcurrentModificationException 的异常,因为迭代器使用的依然是旧数组,只不过迭代的内容可能已经过时了
缺点
- 内存占用问题,内存里会同时驻扎两个对象的内存,这一点会占用额外的内存空间。
- 在元素较多或者复杂的情况下,复制的开销很大,复制过程不仅会占用双倍内存,还需要消耗 CPU 等资源,会降低整体性能。
- 数据一致性问题,由于 CopyOnWrite 容器的修改是先修改副本,所以这次修改对于其他线程来说,并不是实时能看到的,只有在修改完之后才能体现出来
迭代器 COWIterator 类
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}