在JDK1.5中引入了java.util.concurrent包,在该包中定义了一组线程安全的集合,称为并发集合, 这些集合可以作为同步集合的替代品。
非线程安全的集合 | 并发集合 | 共同接口 |
---|---|---|
ArrayList | CopyOnWriteArrayList | List |
LinkedList | ConcurrentLinkedQueue | Queue |
HashSet | CopyOnWriteArraySet | Set |
TreeSet | ConcurrentSkipListSet | SortedSet |
HashMap | ConcurrentHashMap | Map |
TreeMap | ConcurrentSkipListMap | SortedMap |
并发集合实现线程安全的遍历方式有两种:
1、一个是对待遍历集合的快照进行遍历. 快照(Snapshot)是指在创建iterator迭代器对象时给集合内部的数据结构创建一个副本,它反映的是在迭代这一时刻的状态,每个线程在迭代遍历集合时,都会创建本线程的一个副本,就相当于这个快照是线程的特有对象,所以在遍历操作时无须加锁. 另外快照是只读的,因此返回的Iterator迭代器不支持remove()删除操作. 这种快照遍历方式优点是遍历操作与更新操作互不影响,缺点是集合元素非常多时,创建快照开销比较大. CopyOnWriterArrayList与CopyOnWriteArraySet这两个集合采用了快照遍历方式。
2、另一种遍历是准实时遍历,准实际遍历不是针对副本遍历,也不使用锁来保障线程安全,遍历操作与更新可以并发进行,这种遍历方式支持迭代器的remove()操作的, 你删除后可能会在其他线程遍历时立即就反映出来ConcurrentLinkedQueue和ConcurrentHashMap等并发集合采用了这种准实时遍历方式。
并发集合内部在保障线程安全的时候不使用锁,采用CAS操作,或者对锁进行优化,如使用粒度极小的锁.相对于同步集合,使用并发集合的程序的吞吐率提升非常明显,同步集合的程序随着并发数量的增长,会使得集合内部所使用锁的争用所导致的线程上下文切换加剧。
ArrayList集合是使用比较频繁的一个集合, 它底层数据结构是数组,底层是通过数组来存储集合中的元素,它不是线程安全的. 开发多线程程序,可以使用同步集合Vector,或者调用Collections.synchronizedList()把不是线程安全的ArrayList转换为线程安全的。
在很多应用场合下,读操作可能远远大于写操作,希望读操作可以尽可能的快,写操作慢一些也没有关系.读操作不会修改原来的数据,因此每次在读操作进行加锁是一种资源浪费,在同步集合Vector与Collections.synchronizedList()返回的线程安全集合中,每次在读数据时都会进行加锁同步,它们读取数据的效率就低. 根据读写锁思想,读锁与读锁不冲突,读操作会受到写操作的阻碍,在写操作时,读操作必须进行等待,如果在读操作时,写操作也需要等待。
为了将读取的性能发挥到极致,在JDK5中引用了CopyOnWriteArrayList集合, 该集合在读取数据时完全不用加锁,并且写操作也不会阻塞读操作. 从集合类名来看CopyOnWrite就是在写入操作时,进行一次自我复制.即当向集合写入数据并不修改原有的内容, 而是把集合中原来的从容复制到一个副本中,向副本中写入数据,写完后再将副本替换原来的数据。
CopyOnWriteArrayList集合采用快照遍历,在迭代时,不支持删除操作。
ConcurrentLinkedQueue类可以看作是LinkedList类的线程安全版, 可以作为Collections.sychronizedList(LinkedList)替代品. ConcurrentLinkedQueue内部访问共享状态变量(队首与队尾指针)时并不使用锁,而是使用CAS操作来保障线程安全的. ConcurrentLinkedQueue是非阻塞的,避免了上下文切换需要的开销.遍历方式是准实时. 与BlockingQueue阻塞队列比,ConcurrentLinkedQueue更适合更新操作与遍历操作并发的情况, 有若干的线程往/从队列中添加/删除操作,还有若干的线程读取集合中的数据。
ConcurrentLinkedQueue是一个高效的读写队列,在多线程中可以使用BlockingQueue阻塞队列在线程之间共享数据。
BlockingQueue是一个接口,主要有两个实现类ArrayBlockingQueue与LinkedBlockingQueue. ArrayBlockingQueue是基于数组实现的,更适合做有界队列,在队列中存储元素的容量可以在创建队列时指定; LinkedBlockingQueue基于链表实现的,适合做无界队列,因为内部元素可以动态的增加。
BlockingQueue阻塞队列有两个常用的操作:put()与take(). put()方法是将元素添加到队列的尾部,如果队列满了,它会一直等待,直到队列中有空闲的位置; take()会从队列的头部取出一个元素,如果队列为空会一直等待,等到队列中有可用元素再取。
HashTable是一个同步集合,在某个线程操作HashTable期间不允许其他线程参与。
Collections.synchronizedMap(Map)可以返回一个线程安全的集合,采用了装饰器模式,在该线程安全的集合内部,先要获得mutex锁,并发效率低。
ConcurrentHashMap是一种高并发的线程安全的Map集合,可以看作是HashTable的替代品. 在JDK7前,ConcurrentHashMap内部使用粒度极小的锁来保障线程安全,或者说采用了分段锁协议,默认情况下可以支持16个线程并发操作. 在JDK8中对ConcurrentHashMap集合进行了性能提升,采用CAS操作实现线程安全。
跳表是一种可以用来快速查询的数据结构.与平衡树相比,在对跳表插入/删除操作时,只需要对整数数据结构的局部进行操作即可,即在高并发的情况下, 你可能需要一个全局的锁来保障整个平衡树的安全,对于跳表来说,只需要部分锁即可。
使用这一数据结构的集合有: ConcurrentSkipListSet与ConcurrentSkipListMap集合, ConcurrentSkipListSet集合是可以对元素进行排序的线程安全的集合, ConcurrentSkipListMap集合是可以根据键进行排序的线程安全的Map集合。