java中容器主要分为四大类:List,Map,Set,和Queue,但并不是所有的容器都是线程安全的,比如ArrayList,就不是线程安全的,
1.那么如何可以把ArrayList变成线程安全的容器呢?
其实,思路很简单,只要把非线程安全的容器封装在对象内部,然后控制好访问方法即可。
下面我们就以 ArrayList 为例,看看如何将它变成线程安全的。
SafeArrayList<T>{
List<T> c = new ArrayList<>(); //封装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;
}
}
上面封装ArrayList,然后用synchronized锁住访问ArrayList方法,即只能一个线程访问ArrayList,就变成线程安全的了,那么所有的非线程安全的容器不都可以这样实现线程安全么?
2. 同步容器
java SDK想到上面那种情况,提供了该方法。
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();
while (i.hasNext())
foo(i.next());
虽然容器是线程安全的,访问单个方法没有问题,但是组合起来就存在安全问题了,当线程T1访问i.hasNext(),线程T2访问i.next(),那么线程T1在访问i.next()就存在问题了。
正确做法:
List list = Collections. synchronizedList(new ArrayList());
synchronized (list) { //加锁之后只能一个线程访问这两个组合操作了
Iterator i = list.iterator();
while (i.hasNext())
foo(i.next());
}
Java 提供的同步容器还有 Vector、Stack 和 Hashtable,同样是基于 synchronized 实现的,对这三个容器的遍历,同样要加锁保证互斥。
3. 并发容器
java1.5版本之前的所谓的线程安全的容器指的是同步容器。因为是synchronized实现保证互斥,串行化太高,性能太差。
之后 java 1.5版本之后提供了性能更高的容器,一般称为并发容器。
并发容器依然是那四大类:
3.1 List
List 里面只有一个实现类就是 CopyOnWriteArrayList。CopyOnWrite,顾名思义就是写的时候会将共享变量新复制一份出来,这样做的好处是读操作完全无锁。
-
结合下图,理解CopyOnWriteArrayList的原理。
CopyOnWriteArrayList内部维护了一个数组,成员变量array指向数组,,所有的读操作都是基于这个array进行的。如果在遍历的同时,还有一个写操作,例如增加元素9,,它会将原数组复制一份,然后对copy的数组写操作, 执行完,将array指向copy的数组。
-
应用场景:CopyOnWriteArrayList仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致。比如上面的操作,写入的元素并不能被立即遍历到。
-
注意:CopyOnWriteArrayList 迭代器是只读的,不支持增删改。因为迭代器遍历的仅仅是一个快照,而对快照进行增删改是没有意义的。
3.2 Map
Map 接口的两个实现是 ConcurrentHashMap 和 ConcurrentSkipListMap。
- 区别: ConcurrentHashMap 的key是无序的。 ConcurrentSkipListMap 的 key 是有序的。
- 下面这个表格总结了 Map 相关的实现类对于 key 和 value 的要求。
- 性能:ConcurrentSkipListMap 里面的 SkipList 本身就是一种数据结构,中文一般都翻译为“跳表”。跳表插入、删除、查询操作平均的时间复杂度是 O(log n),理论上和并发线程数没有关系,所以在并发度非常高的情况下,若你对 ConcurrentHashMap 的性能还不满意,可以尝试一下 ConcurrentSkipListMap。
3.3 Set
Set 接口的两个实现是 CopyOnWriteArraySet 和 ConcurrentSkipListSet,
- 使用场景可以参考前面讲述的 CopyOnWriteArrayList(读多写少) 和 ConcurrentSkipListMap(key有序),它们的原理都是一样的,这里就不再赘述了。
3.4 Queue
java并发包中队列可以分为两个维度来分类:
- 阻塞和非阻塞:阻塞是指队列满了,入队操作阻塞,队列空是,出队操作阻塞。Java并发包中用Blocking关键字表示阻塞队列。
- 单端和双端:单端只能队尾入队,队首出队;双端是队尾和队首都可以入队出队。单端队列用Queue标识,双端队列用Deque标识
两个维度组合后分为四大类:
- 单端阻塞队列:实现有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue 和 DelayQueue。
- ArrayBlockingQueue:内部一般会持有一个数组队列。
- LinkedBlockingQueue:内部一般会持有一个链表队列。
- SynchronousQueue:不持有队列,生产者消费者模式,生产者的线程入队操作必须等待消费者线程的出队操作。
- LinkedTransferQueue:融合LinkedBlockingQueue和SynchronousQueue的功能,性能比LinkedBlockingQueue好。
- PriorityBlockingQueue 支持按照优先级出队。
- DelayQueue 支持延时出队。
- 双端阻塞队列:其实现是 LinkedBlockingDeque。
- 单端非阻塞队列:其实现是 ConcurrentLinkedQueue。
- 双端非阻塞队列:其实现是 ConcurrentLinkedDeque。
注意:上面这些队列中只有 ArrayBlockingQueue 和 LinkedBlockingQueue 是支持有界的,所以在使用其他无界队列时,一定要充分考虑是否存在导致 OOM 的隐患。
一般工作中不建议使用无界的队列。