一、同步容器与并发容器
1. 同步容器
早起的JDK中,有两种现成的同步容器,Vector和HashTable,可以直接new对象获取 ;
在JDK1.2中,引入了同步封装类,可以由Collections.synchronizedXxxx等方法创建;
同步容器的问题
- HashTable 效率低
- 符合操作存在安全问题,需要额外加锁保护;
常见复合操作如下:
迭代:反复访问元素,直到遍历完全部元素;
跳转:根据指定顺序寻找当前元素的下一个(下n个)元素;
条件运算:例如若没有则添加等
例子:
if (!table.contants()) {
table.put();
}
迭代器及快速失败问题
同步容器与非同步容器一样,在迭代期间,若其它线程并发修改该容器,会抛出ConcurrentModificationException异常,即快速失败机制。
实现原理
通过计数器记录容器修改次数,在迭代时,如hasNext等方法调用时遇到计数器数值变化则抛出异常。
解决
通过在容器迭代期间对容器加锁来解决该问题是一种方式,但并发性差,当容器规模大时,更加严重,而且还可能产生死锁问题;一种更优的解决方式,如上面链接里提到的,采用克隆容器(CopyOnWriteArrayList等),在副本上进行操作,但存在显著的性能开销,需要拷贝数组等操作,这种方式的好坏要看具体需求,如容器大小,执行的具体操作,调用频率等,一般当迭代操作远多于修改操作时,比较适用克隆容器;
隐藏的迭代操作
另外,在集合中,有一些隐藏的迭代操作,如toString,equals,hashCode等方法,使用时需注意,也可能会抛出ConcurrentModificationException异常;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
/** * CopyOnWriteArrayList/CopyOnWriteArraySet:写入并复制。 * @author xiaobin * @date 2018/3/5 */
public class TestCopyOnWriteArrayList {
public static void main(String[] args) {
HelloTread ht = new HelloTread();
for (int i = 0 ; i < 10; i++) {
new Thread(ht).start();
}
}
}
class HelloTread implements Runnable {
private static List<String> list = Collections.synchronizedList(new ArrayList<String>());
static {
list.add("AA");
list.add("BB");
list.add("CC");
}
@Override
public void run() {
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
//迭代期间容器发生变化,fail-fast
list.add("AA");
}
}
}
2. 并发容器
在Java 5.0 后,java.util.concurent包中提供了多种并发容器类来改进同步容器的性能。
如:
ConcurrentHashMap是一个线程安全的哈希表。在多线程访问时优于HashMap
ConcurrentSkipListMap 优于TreeMap
ConcurrentSkipListSet
CopyOnWriteArrayList 当期望的读数和遍历远远大于列表更新数时优于ArrayList。如:大量监听器时使用
CopyOnWriteArraySet
CopyOnWriteArrayList
写入并复制;每次添加时,都会复制一个容器的副本,再添加。当多个线程并发操作多时使用此容器。
上面例子修改容器为CopyOnWriteArrayList避免了fail-fast。
private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
ConcurrentHashMap
并发容器包下的ConcurrentHashMap也是采用CAS操作,同时应用锁分段机制,提供了访问效率。
原子操作
增加了若干原子操作方法,如putIfAbsent(没有该key,则添加);
如果不存在(新的entry),那么会向map中添加该键值对,并返回null。
如果已经存在,那么不会覆盖已有的值,直接返回已经存在的值。
迭代器弱一致性
迭代期间不会抛出ConcurrentModificationException异常;
size()、isEmpty()等方法返回的是一个近似值;