目录:
这一章主要介绍关于 Java 中同步并发的相关类,干货非常的多。介绍包括了:
同步容器
1、Vector
2、HashTable
并发容器
1、ConcurrentHashMap
2、CopyOnWriteArrayList
同步容器
效率低
同步容器的概念就是说对容器状态的访问进行了串行化,来实现它们的安全性。这种串行的实现方法会严重的影响并发性能。
例如 Vector 和 HashTable 就是这种容器,它们分别是 ArrayList 和 HashMap 的线程安全版本,并且内部对很多方法都使用了 synchronized 修饰。
对于 synchronized 修饰后的方法,就意味着只能有一个线程访问该对象的同步方法。这样子的话即使两个线程同时进行 read 操作也是不允许的。所以说效率比较低。
复合操作容易出现问题
如 第2章:使用线程安全类不一定能保证线程是安全的 说到的,复合操作 Vector,HashTable 不一定能保证其安全性。
迭代的时候不能增删
另外,书上也提到了迭代的时候增删元素会报 ConcurrentModification 异常,这个其实也算是复合操作的一种吧,主要还是因为这个集合的计数器发生了变化,比如增删导致计数器+1或者-1,那么迭代器 hashNext() 或者 next() 的时候就会抛异常,属于一种 “fail fast”,例如:
public class Test {
public static void main(String args[]){
Vector<Integer> vector = new Vector<Integer>(3);
vector.add(1);
vector.add(2);
vector.add(3);
new Thread(() ->{
vector.remove(0);
try {
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Iterator<Integer> iterator = vector.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
try {
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
那么此时也会报:
1
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.Vector$Itr.checkForComodification(Vector.java:1184)
at java.util.Vector$Itr.next(Vector.java:1137)
at Test.main(Test.java:30)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
并发容器
同步容器是一早就出了,在 JDK5 中出了一些并发容器来替代同步容器。例如 CopyOnWriteArrayList 主要替代 同步的List ,以及 ConcurrentHashMap 主要替代同步的 Map。
ConcurrentHashMap
ConcurrentHashMap 主要采用一种分段锁,例如存储的 table 数组分为x段,每一个段用每一段的锁,各不影响,这样它能够支持并发的读,以及一定程度的并发的写,在并发环境下会有更高的吞吐量。
另外,它也能支持一些复合操作,这也是它的优点之一,如:
//没有就添加
public V putIfAbsent(K key, V value) {
return putVal(key, value, true);
}
另外,ConcurrentHashMap 也不会抛出如 ConcurrentModificationException 异常,主要是因为它返回的迭代器具有弱一致性,而不是 “fail fast”。至于为什么是弱一致性,这里有说到:为什么ConcurrentHashMap是弱一致的,copy 过来,就是:
迭代器实际上是在遍历底层数组。在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap迭代器弱一致的表现。
CopyOnWriteArrayList
它实际是也是 ArrayList 的一种变体。它比 Vector 好在 get 操作没有并发控制,如:
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
而 Vector 的 get 操作是这样子的:
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
它这样做的一个好处这里也说得比较好:聊聊并发-Java中的Copy-On-Write容器
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
另外,为什么说是 CopyOnWrite 呢,看它的 add 方法就知道了:
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();
}
}
加锁是为了防止多个线程同时的修改,那么具体的修改逻辑是通过创建一个新的数组。这样其实会有一个问题,就是 read 操作的时候,有可能 read 到老的值。
当然,由于是 copy 底层数组,所以说增删操作的时候效率是非常低的。
最后,对于迭代,自然也不会出现 ConcurrentModificationException,因为写是创建一个新的数组,而不会影响原来的数组。
总结
所以说,对于容器部分,一般会有:
同步容器
Vector
HashTable
并发容器
ConcurrentHashMap
CopyOnWriteArrayList
那么,真正在使用的时候,可以选取 ConcurrentHashMap 和 CopyOnWriteArrayList 分别应用在 Map 和 List ,因为效率上还是要比 HashTable 和 Vector 高得多。