并发安全的集合(CopyOnWriteArrayList、ConcurrentHashMap)

1、CopyOnWriteArrayList(重要)

CopyOnWriteArrayList 是JUC包中提供的一个并发容器。CopyOnWriteArrayList 相当于线程安全的 ArrayList。

底层原理:
CopyOnWriteArrayList 使用了一种叫写时复制的方法。
当有新元素 add 到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来。然后在新的数组上执行操作,操作完之后,将原来的数组引用指向到新数组。(执行add方法需要加锁是为了保证同步,避免多个线程同时写时复制出多个副本)

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    //加锁
    lock.lock();
    try {
        //获取原有的数据数组
        Object[] elements = getArray();
        int len = elements.length;
        //拷贝---长度为len + 1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //将新添加的数据设置到最后
        newElements[len] = e;
        //将array指向新的newElements --内存可见(array变量被volatile修饰)
        setArray(newElements);
        return true;
    } finally {
        //释放锁
        lock.unlock();
    }
}    

对 CopyOnWriteArrayList 执行读操作时不需要加锁。
因此,CopyOnWriteArrayList 适合用于读多写少的并发场景中,例如缓存等。

优点:
1、采用读写分离,因此读操作的性能很高
2、牺牲数据的实时性而保证了最终数据的一致性。即读线程对数据的更新是延迟感知的,因此这种情况下读线程不存在等待的情况。

缺点:
1、内存占用的问题:因为CopyOnWrite采用的是写时复制机制,所以在写操作的时候。内存中会存在两个对象的内存,旧的对象和新写入的对象。注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)如果对象内存占用比较大有可能造成频繁的GC。
2、CopyOnWrite容器保证最终的数据一致性而不能保证数据的实时一致性

2、ConcurrentHashMap(重要)

ConcurrentHashMap 是线程安全且高效的 HashMap。

在并发编程中使用HashMap可能导致程序死循环。而使用线程安全的HashTable效率又非常低下,基于以上两个原因,便有了ConcurrentHashMap 的登场机会。

1、线程不安全的HashMap

在多线程环境下,使用HashMap进行put操作会形成循环链表,引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。

2、效率低下的HashTable

HashTable容器使用sychronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

3、ConcurrentHashMap

1、JDK 7中的实现:

HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的分段锁技术。首先,将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构构成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap中扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。
在这里插入图片描述
Segment 数组不能扩容(默认为16),扩容是 Segment 数组某个位置内部的数组 HashEntry<K,V>[] 进行扩容,扩容后,容量为原来的 2 倍。

优点:
1、减少Hash冲突,避免一个槽里有太多元素。
2、提高读和写的并发度,段与段之间相互独立。
3、提高了扩容的并发度,它不是整个ConcurrentHashMap一起扩容,而是每个 Segment 独立扩容。

2、JDK 8中的实现:

JDK 8 的实现已经摒弃了 Segment 的概念,而是直接用 数组+链表+红黑树 的数据结构来实现,并发控制使用 Synchronized 和 CAS 来操作,整个看起来就像是优化过且线程安全的 HashMap。
在这里插入图片描述
更小的锁粒度
1.8中摒弃了Segment锁,直接将hash桶的头结点当做锁。旧版本的一个Segment锁,保护了多个hash桶,而1.8版本的一个锁只保护一个hash桶,由于锁的粒度变小了,写操作的并发性得到了极大的提升。

总结ConcurrentHashMap在JDK7和8的异同点和优缺点

1、数据结构
JDK 7 采用 Segment分段锁 + 哈希表 实现
JDK 8 中的 ConcurrentHashMap 使用 数组 + 链表 + 红黑树

2、并发度
JDK 7 中,每个 Segment 独立加锁,最大并发个数就是 Segment 的个数,默认是 16。
JDK 8 中,锁粒度更细。理想情况下table 数组元素的个数(也就是数组长度)就是其支持并发的最大个数,并发度比之前有提高。

3、线程安全
JDK 7 采用 Segment分段锁来保证安全,而 Segment 是继承自ReentrantLock。
JDK 8 中放弃了 Segment 的设计,采用 数组 + CAS + Synchronized 保证线程安全。

4、Hash 碰撞
JDK 7 在 Hash 冲突时,会使用拉链法,也就是链表的形式。
JDK 8 先使用拉链法,在链表长度超过一定阈值时,将链表转换为红黑树,来提高查找效率。

5、查询时间复杂度
JDK 7 遍历链表的时间复杂度是 O(n),n 为链表长度。
JDK 8 如果变成遍历红黑树,那么时间复杂度降低为 O(log(n)),n 为树的节点个数。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小本科生debug

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值