Java并发20-并发容器中的坑
Java中的容器主要可分为四个大类,分别是List
、Map
、Set
和Queue
。
如何将非线程安全容器变成线程安全—Java并发12:把非线程安全容器封装在对象内部,然后控制好访问路径。
SafeArrayList<T>{
//封装ArrayList
List<T> c = new ArrayList<>();
//控制访问路径
synchronized T get(int idx){
return c.get(idx);
}
synchronized void add(int idx, T t) {
c.add(idx, t);
}
synchronized boolean addIfNotExist(T t){
if(!c.contains(t)) {
c.add(t);
return true;
}
return false;
}
}
JDK做了类似的包装:
List list = Collections.synchronizedList(new ArrayList());
Set set = Collections.synchronizedSet(new HashSet());
Map map = Collections.synchronizedMap(new HashMap());
注意竞态条件
切记:组合操作需要注意竞态条件问题
List list = Collections.synchronizedList(new ArrayList());
Iterator i = list.iterator();
//这里就产生了竞态条件 执行5行的时候 可能已经无next()了。
while (i.hasNext())
foo(i.next());
正确做法:
List list = Collections.synchronizedList(new ArrayList());
synchronized (list) {
Iterator i = list.iterator();
while (i.hasNext())
foo(i.next());
}
SychronizedList
、SychronizedMap
、SychronizedSet
、Vector
、Stack
、HashTable
都是sychronized
关键字实现的,称为同步容器。
Vector
、Stack
和Hashtable
这三个容器不是基于包装类实现的,对这三个容器的遍历,同样要加锁保证互斥。
并发容器
JDK1.5之前主要指同步容器线程安全,但性能差。1.5之后提供了性能更好的并发容器,并发容器遍历线程安全。
List
List 里面只有一个实现类就是CopyOnWriteArrayList
。
CopyOnWriteArrayList
内部维护了一个数组,成员变量array
就指向这个内部数组,所有的读操作都是基于 array
进行的,如下图所示,迭代器Iterator
遍历的就是array
数组。
如果遍历array
时,还有一个写操作,例如增加元素,CopyOnWriteArrayList
会将array
复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将array
指向新的数组。
Map
Map
接口的两个实现是ConcurrentHashMap
和ConcurrentSkipListMap
。
ConcurrentHashMap
的key
无序,ConcurrentSkipListMap
的key
有序。
它们的key
和value
都不能为空。
ConcurrentSkipListMap
里面的SkipList
本身就是一种数据结构。
跳表插入、删除、查询操作平均的时间复杂度是O(log n)
,理论上和并发线程数没有关系,所以在并发度非常高的情况下,若ConcurrentHashMap
的性能还不够满意,可以尝试ConcurrentSkipListMap
。
Set
Set
接口的两个实现是CopyOnWriteArraySet
和ConcurrentSkipListSet
,使用场景可以参考前面讲述的 CopyOnWriteArrayList
和ConcurrentSkipListMap
,它们的原理都是一样的。
Queue
阻塞与非阻塞:当队列已满,入队操作阻塞;当队列已空时,出队操作阻塞。
单端与双端:单端即队尾入队,队首出队;双端即队首队尾皆可入队出队。
Java并发包里阻塞队列都用Blocking
关键字标识,单端队列使用Queue
标识,双端队列使用Deque
标识。
单端阻塞队列
ArrayBlockingQueue
内部持有一个数组队列。
LinkedBlockingQueue
内部持有一个链表队列。
SynchronousQueue
内部不持有队列,生产者-消费者模式下,生产者线程的入对操作必须等待消费者线程的出队操作。
LinkedTransferQueue
(大佬工作中没用过)结合了
LinkedBlockingQueue
&SynchronousQueue
功能,性能比LinkedBlockingQueue
好。PriorityBlockingQueue
支持按照优先级出队。
DelayQueue
支持延时出队。
双端阻塞队列
LinkedBlockingDeque
单端非阻塞队列
ConcurrentLinkedQueue
双端非阻塞队列
ConcurrentLinkedDeque
实际工作中,一般都不建议使用无界的队列(即没有设置队列容量),因为数据量大了之后很容易导致 OOM。
只有ArrayBlockingQueue
和LinkedBlockingQueue
是支持有界的。
要学会根据容器特性选择才是关键。
思考
线上系统 CPU 突然飙升,怀疑在并发场景里使用了HashMap
,因为在1.8之前的版本里并发执行HashMap.put()
可能会导致CPU飙升到100%,该如何验证猜测?
Java7中的HashMap在执行put操作时会涉及到扩容,由于扩容时链表并发操作会造成链表成环,所以可能导致cpu飙升100%。
——黑白尤文
为何concurrentskiplistmap
比concurrenthashmap
性能好?
如果key冲突比较大,hashmap
还是要靠链表或者tree
来解决冲突的,所以O(1)是理想值。
同时增删改操作很多也影响hashmap
性能。这个也是要看冲突情况,也就是说hashmap
的稳定性差。
如果很不幸正好偶遇它的稳定性问题,同时又接受不了,就可以尝试skiplistmap
,它能保证稳定性,无论并发量是多大,也没有key冲突的问题。