Java 中的线程安全容器
一、同步容器
常用的一些容器例如 ArrayList、HashMap、都不是线程安全的,最简单的将这些容器变为线程安全的方式,是给这些容器所有的方法都加上 synchronized 关键字。
Java 的 Collections 中实现了这些同步容器:
简单的使用如下:
List<String> list = Collections.synchronizedList(new ArrayList<>()); Map<Integer, String> map = Collections.synchronizedMap(new HashMap<>()); Set<String> set = Collections.synchronizedSet(new HashSet<>());
同步容器虽然简单,但是相应的效率较低,因为锁的粒度较大。
循环遍历同步容器
如果在遍历同步容器的时候,组合了多个方法,这会可能会存在竞态条件,仍然不是线程安全的。解决的办法便是对容器加锁。例如下面这样:
public static void main(String[] args) { List<String> list = Collections.synchronizedList(new ArrayList<>()); //省略添加数据的操作 String[] str = new String[list.size()]; int k = 0; synchronized (list){ Iterator<String> iterator = list.iterator(); while (iterator.hasNext()){ str[k ++] = iterator.next(); } } }
二、并发容器
Java 中还提供了一系列并发容器,相比于同步容器,其性能更好。并发容器共分为了四类:List、Map、Set、Queue。
1. List
List 中一个最主要的实现类是 CopyOnWriteArrayList
,CopyOnWrite,即写时复制,这样的好处是读操作是无锁的。
其实现原理是内部维护了一个数组,内部变量 array 指向了这个数组。需要写时,并不是在原数组上操作,而是将数组复制一份,在拷贝的数组中进行写。完成后,将 array 指向新的数组。这样一来,读写之间不互斥,效率得到了很大的提升。
需要注意的是 CopyOnWriteArrayList 适用于读多写少的场景,并且需要接受读写的暂时不一致,因为在写的时候,并行的读操作可能并不能马上看到写的结果。
2. Map
Map 的两个主要实现类是 ConcurrentHashMap
和 ConcurrentSkipListMap
,两者主要的区别是:前者是无序的,后者是有序的。
2.1 ConcurrentHashMap
在 Java 1.7 中,ConcurrentHashMap 的实现使用的是分段锁技术,其内部主要的数据结构是 Segment 和 HashEntry,ConcurrentHashMap 包含了一个 Segment 数组,每个 Segment 又包含一个 HashEntry 数组,每个 HashEntry 是一个存储数据的链表结构。
其中 Segment 继承了 ReentrantLock,每个 Segment 都有对应的锁,需要修改数据的时候,需要获取这把锁。修改不同的 Segment 数据,则完全可以并行,效率得到了提升。示意图如下:
Java 1.8 又对 ConcurrentHashMap 做了较大的改进,放弃了分段锁的技术。结构和 Java 1.8 中的 HashMap 类似,采用的是数组+链表/红黑树来实现。为了降低哈希冲突的成本,在链表长度超过 8 时,将链表转换为红黑树。使用 CAS 和 synchronized 解决并发问题,锁住链表或者红黑树的头节点,只要没有哈希冲突,则不会出现并发问题。示意图如下:
2.2 ConcurrentSkipListMap
ConcurrentSkipListMap 保证有序的主要原因是,底层使用的是跳表这种数据结构,关于跳表的介绍,你可以查看数据结构中的内容。
3. Set
Set 的两个实现是 CopyOnWriteArraySet
和 ConcurrentSkipListSet
。
和前面说到的 CopyOnWriteArrayList 、ConcurrentSkipListMap 实现的原理类似。
4. Queue
队列可以从两方面进行分类:
- 单端和双端:单端队列指的是只能在队首出队,队尾出队,而双端队列指的是队首和队尾均可入队和出队。
- 阻塞和非阻塞:阻塞队列指的是,当队列满的时候,入队列阻塞;当队列空的时候,出队列阻塞。
Java 中,单端队列使用 queue 标识,双端队列使用 deque 标识。
4.1 单端阻塞队列
常用的实现类有:ArrayBlockingQueue
、LinkedBlockingQueue
、PriorityQueue
。
4.2 单端非阻塞队列
其实现类是 ConcurrentLinkedQueue
4.3 双端阻塞队列
其实现类是 LinkedBlockingDeque
4.4 双端非阻塞队列
其实现类是 ConcurrentLinkedDeque
使用其实都非常地简单,就是入队出队之类的操作,这里不再赘述了。