同步集合和并发集合

同步集合和并发集合

同步集合类:Hashtable、Vector 方法上有同步约束 (jdk1.0)

同 步 集 合 包 装 类 : Collections.synchronizedMap(new HashMap<>()) 和Collections.synchronizedList(new ArrayList<>()) —使用的是全局锁

并发集合类:ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteHashSet
性能比较
同步集合比并发集合会慢得多,主要原因是锁,同步集合会对整个 Map 或 List 加锁

并发集合的实现原理

ConcurrentHashMap[jdk1.7]把整个 Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段。CopyOnWriteArrayList 允许多个线程以非同步的方式读,当有线程写的时候它会将整个List 复制一个副本给它。如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。
并发集合的使用建议一般不需要多线程的情况,只用到 HashMap、ArrayList,只要真正用到多线程的时候就一定要考虑同步。所以这时候才需要考虑同步集合或并发集合。
解决方法 1: 给整个集合上添加一个锁

 List<Integer> list=Collections.synchronizedList(new ArrayList<>());

解决方法 2:适用于读多写少的场景下

 List<Integer> list=new CopyOnWriteArrayList<>();

ConcurrentHashMap 实现原理
ConcurrentHashMap (JDK1.7) 是由 Segment 数组结构和 HashEntry 数组结构组成。Segment 是一种可重入锁 ReentrantLock,在 ConcurrentHashMap 里扮演锁的角色,HashEntry 则用于存储键值对数据。
一个 ConcurrentHashMap 里包含一个 Segment 数组,Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry是一个链表结构的元素, 每个 Segment 守护者一个 HashEntry 数组里的元素,当对HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。
在这里插入图片描述
JDK1.7 版本的 CurrentHashMap 的实现原理
在 JDK1.7 中 ConcurrentHashMap 采用了【数组+Segment 分段锁】的方式实现。

  • Segment(分段锁) ConcurrentHashMap 中的分段锁称为 Segment,它即类似于
    HashMap 的结构,即内部拥有一个 Entry 数组,数组中的每个元素又是一个链表,同时又是一个 ReentrantLock(Segment 继承了 ReentrantLock)。
  • 内部结构。 ConcurrentHashMap 使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。ConcurrentHashMap 定位一个元素的过程需要进行两次 Hash 操作。第一次 Hash 定位到 Segment,第二次 Hash 定位到元素所在的链表的头部。

坏处: 这一种结构的带来的副作用是 Hash 的过程要比普通的 HashMap 要长
好处: 写操作的时候可以只对元素所在的 Segment 进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap 可以最高同时支持 Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的 Segment 上)。所以通过这种结构,ConcurrentHashMap 的并发能力可以大大的提高。
在这里插入图片描述
JDK8 中 ConcurrentHashMap 采用了【数组+链表+红黑树】的实现方式来设计,内部大量采用 CAS 操作。
JDK8 中彻底放弃了 Segment 转而采用的是 Node,其设计思想也不再是 JDK1.7 中的分段锁思想。
Node:保存 key,value 及 key 的 hash 值的数据结构。其中 value 和 next 都用 volatile修饰,保证并发的可见性。
在 JDK8 中 ConcurrentHashMap 的结构,由于引入了红黑树,使得 ConcurrentHashMap的实现非常复杂,红黑树是一种性能非常好的二叉查找树,其查找性能为 O(log2N),但是其实现过程也非常复杂,而且可读性也非常差,早期完全采用链表结构时 Map 的查找时间复杂度为 O(N),JDK8 中 ConcurrentHashMap 在链表的长度大于某个阈值的时候会将链表转换成红黑树进一步提高其查找性能。

CAS 是 compare and swap 的缩写,即比较交换。cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。

CAS 操作包含三个操作数 —— 内存位置 V、预期原值 A 和新值 B。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS 是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被 b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。

CopyOnWrite 容器
CopyOnWrite 容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList 的实现原理
可以发现在添加的时候是需要加锁的,否则多线程写的时候会 Copy 出 N 个副本出来。读的时候不需要加锁,如果读的时候有多个线程正在向 ArrayList 添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的 ArrayList。

CopyOnWrite 的应用场景
CopyOnWrite 并发容器用于读多写少的并发场景。
比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。

CopyOnWrite 的缺点
1、内存占用问题。因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说 200M 左右,那么再写入100M 数据进去,内存就会占用 300M,那么这个时候很有可能造成频繁的 Yong GC 和 Full GC。系统中使用了一个服务由于每晚使用 CopyOnWrite 机制更新大对象,造成了每晚 15秒的 Full GC,应用响应时间也随之变长。针对内存占用问题,可以通过压缩容器中的元素的方法来少大对象的内存消耗,如果元素全是 10 进制的数字,可以考虑把它压缩成 36 进制或 64 进制。或者不使用 CopyOnWrite容器,而使用其他的并发容器,如 ConcurrentHashMap。

2、数据一致性问题。CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用 CopyOnWrite 容器。

猜你喜欢

转载自blog.csdn.net/Lecheng_/article/details/115095323
今日推荐