什么是并发容器的实现?
何为同步容器?可以简单地理解为通过 synchronized
来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。
- 比如 Vector,Hashtable,以及
Collections#synchronizedSet()
,Collections#synchronizedList()
等方法返回的容器。 - 可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字
synchronized
。
并发容器,使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性。
- 例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁。在这种锁机制下,允许任意数量的读线程并发地访问 map ,并且执行读操作的线程和写操作的线程也可以并发的访问 map ,同时允许一定数量的写操作线程并发地修改 map ,所以它可以在并发环境下实现更高的吞吐量。
- 再例如,CopyOnWriteArrayList 。
SynchronizedMap 和 ConcurrentHashMap 有什么区别?
- SynchronizedMap
- 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map 。
- ConcurrentHashMap
- 使用分段锁来保证在多线程下的性能。ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将 hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。【注意,这块是 JDK7 的实现。在 JDK8 中,具体的实现已经改变】
- 另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当 iterator 被创建后集合再发生改变就不再是抛出 ConcurrentModificationException 异常,取而代之的是在改变时
new
新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator 线程可以使用原来老的数据,而写线程也可以并发的完成改变。
关于 ConcurrentHashMap 的源码解析,推荐胖友看看如下两篇文章:
🦅 Java 中 ConcurrentHashMap 的并发度是什么?
在 JDK8 前,ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16 ,这样在多线程情况下就能避免争用。
在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧。
🦅 ConcurrentHashMap 为何读不用加锁?
在 JDK7 以及以前
- HashEntry 中的
key
、hash
、next
均为final
型,只能表头插入、删除结点。- HashEntry 类的
value
域被声明为volatile
型。 - 不允许用
null
作为键和值,当读线程读到某个 HashEntry 的value
域的值为null
时,便知道产生了冲突——发生了重排序现象(put 方法设置新value
对象的字节码指令重排序),需要加锁后重新读入这个value
值。
- HashEntry 类的
-
volatile
变量count
协调读写线程之间的内存可见性,写操作后修改count
,读操作先读count
,根据 happen-before 传递性原则写操作的修改读操作能够看到。
在 JDK8 开始
- Node 的
val
和next
均为volatile
型。 -
#tabAt(..,)
和#casTabAt(...)
对应的 Unsafe 操作实现了volatile
语义。
CopyOnWriteArrayList 可以用于什么应用场景?
CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException 异常。在 CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
- 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 ygc 或者 fgc 。
- 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。
CopyOnWriteArrayList 透露的思想:
- 读写分离,读和写分开
- 最终一致性
- 使用另外开辟空间的思路,来解决并发冲突
CopyOnWriteArrayList 适用于读操作远远多于写操作的场景。例如,缓存。
关于 CopyOnWriteArrayList 的源码,可以看看 《CopyOnWriteArrayList 实现原理及源码分析》 文章。
网友评论