Java1.5是synchronized分水岭
同步容器及其注意事项
Java中的容器主要可以分为四个大类,分别是List、Map、Set和Queue,但并不是所有的Java容器都是线程安全的。常用的ArrayList、HashMap就不是线程安全的。
把非线程安全的容器封装在对象内部,然后控制好访问路径,就可以将非线程安全的容器变成线程安全的容器。(面向对象思想)
1 | SafeArrayList<T> { |
所有非线程安全的类都可以用这种包装的方式来实现线程安全。
Collections中提供了一套完备的包装类。
1 | List list = Collections.synchronizedList(new ArrayList()); |
组合操作需要注意竞态条件问题。如addIfNotExit()方法就包含组合操作。组合操作往往隐藏着竞态条件问题,即便每个操作都能保证原子性,也并不能保证组合操作的原子性。
在容器领域一个容易被忽视的坑是用迭代器遍历容器。
在遍历的时候出现修改操作,直接抛快速失败(Fail-Fast)异常。
1 | List list = Collections.synchronizedList(new ArrayList()); |
通过迭代器遍历容器list,对每个元素调用foo()方法,存在并发问题。
正确的做法是,锁住list之后再执行遍历操作。Collections内部的包装类,公共方法锁的是对象的this,就是下面的list,锁住list绝对是线程安全的。
1 | List list = Collections.synchronizedList(new ArrayList()); |
这些经过包装后线程安全的容器,都是基于synchronized这个同步关键字实现的,所以也被称为同步容器。
Java提供的同步容器还有Vector、Stack和Hashtable,这三个容器不是基于包装类实现的,但同样是基于synchronized实现的,对这三个容器的遍历,同样要加锁保证互斥。
并发容器及其注意事项
Java在1.5版本之前线程安全的容器,主要指的是同步容器,所有方法都用synchronized来保证互斥,串行度高、性能差。
Java在1.5及之后版本提供了性能更高的容器-并发容器。
依然是四大类:List、Set、Map、Queue

List
List里面只有一个实现类CopyOnWriteArrayList。
CopyOnWrite-写的时候会将共享变量新复制一份出来,读操作完全无锁。
CopyOnWriteArrayList内部维护了一个数组,成员变量array指向这个内部数组,所有的读操作都是基于array进行的。(迭代器Iterator遍历的就是array数组)。
如果在遍历array的同时,还有一个写操作(如增加元素),CopyOnWriteArrayList会将array复制一份,然后在新复制的数组上执行增加元素的操作,执行完之后再将array指向这个新的数组。
读写是可以并行的,遍历操作一直都是基于原array执行,而写操作则是基于新array进行。
使用CopyOnWriteArrayList需要注意的坑有两个方面。
- 应用场景,仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致。(写入的新元素并不能立刻被遍历到。)
- 迭代器是只读的,不支持增删改。因为迭代器遍历的仅仅是一个快照,而对快照进行增删改是没有意义的。
Map
Map接口的两个实现是ConcurrentHashMap(key无序)和ConcurrentSkipListMap(key有序)。
SkipList-跳表。跳表插入、删除、查询操作平均的时间复杂度是O(logn),性能好。

Set
Set接口的两个实现是CopyOnWriteArraySet和ConcurrentSkipListSet。
使用场景类似CopyOnWriteArrayList和ConcurrentSkipListMap。
Queue
Queue这类并发器是最复杂的,从两个维度来分类。
- 阻塞与非阻塞。当队列已满时,入队操作阻塞;当队列已空时,出队操作阻塞。
- 单端与双端。单端指的是只能队尾入队,队首出队;双端指的是队首队尾皆可入队出队。
Java并发包里阻塞队列队列都用Blocking关键字标识,单端队列使用Queue标识,双端队列使用Deque标识。
单端阻塞队列
ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue、DelayQueue。
内部一般会持有一个队列,这个队列可以是数组(ArrayBlockingQueue)也可以是链表(LinkedBlockingQueue);
甚至还可以不持有队列(SynchronousQueue),此时生产者线程的入队操作必须等待消费者线程的出队操作。
LinkedTransferQueue融合LinkedBlockingQueue和SynchronousQueueQueue的功能,性能比LinkedBlockingQueue更好;
PriorityBlockingQueue支持按照优先级出队;
DelayQueue支持延时出队。双端阻塞队列
LinkedBlockingDeque单端非阻塞队列
ConcurrentLinkedQueue双端非阻塞队列
ConcurrentLinkedDeque
使用队列时,需要格外注意队列是否支持有界(所谓有界指的是内部的队列是否有容量限制)。实际工作中,一般都不建议使用无界的队列,数据量大了之后很容易导致OOM。
上述Queue中,ArrayBlockingQueue和LinkedBlockedQueue是支持有界的。