多线程并发 - CopyOnWrite 容器

一、CopyOnWrite 简介

Copy-On-Write 简称 COW,是计算机设计领域中的一种优化策略,也是一种在并发场景下常用的设计思想——写入时复制思想

JDK1.5 开始Java并发包里提供了两个使用 CopyOnWrite 机制实现的并发容器,它们是CopyOnWriteArrayListCopyOnWriteArraySet

特点:

  • 读取安全(不保证缓存一致性),写入安全(代价是加了锁,且需全量复制)
  • 适用于对象空间占用大,修改次数少,而且对数据实效性要求不高的场景
  • 不建议用于频繁读写场景下,全量复制很容易造成GC停顿,因此建议使用平时的Concurrent包来实现

CopyOnWrite 容器即写时复制的容器,当我们往一个容器中添加元素的时候,不直接往容器中添加,而是将当前容器进行copy,复制出来一个新的容器,然后向新容器中添加我们需要的元素,最后将原容器的引用指向新容器。

这样做的好处在于,我们可以在并发的场景下对容器进行"读操作"而不需要"加锁",从而达到读写分离的目的

二、CopyOnWriteArrayList

1、介绍

CopyOnWriteArrayList常被用于 读多写少 的并发场景;因为 CopyOnWriteArrayList 无需任何同步措施,大大增强了读的性能

当遍历 ArrayListLinkedList 的时候,若中途有别的线程对List容器进行修改,则会抛出 ConcurrentModificationException 异常。而CopyOnWriteArrayList 由于其 读写分离机制,遍历和修改操作分别作用在不同的 List 容器,所以在使用迭代器遍历的时候,则不会抛出异常。

缺点:
内存占用问题CopyOnWriteArrayList 每次执行写操作都会将原容器进行拷贝一份,内存里会同时有两个对象,旧对象和新写入的对象;当数据量大的时候,内存会存在较大的压力,可能会造成频繁的 GC问题。
例如:假设此时对象占用100M,那再写入100M数据时,内存就会多占用200M。
数据一致性问题:由于实现的原因,写和读分别作用在不同新老容器上,在写操作执行过程中,读不会阻塞,但读取到的却是老容器的数据;CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性

2、原理【源码分析】

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
    private static final long serialVersionUID = 8673264195747942595L;
    final transient ReentrantLock lock = new ReentrantLock();
    private transient volatile Object[] array;
    private static final Unsafe UNSAFE;
    private static final long lockOffset;
	
	//....省略其他方法
}    

上述源码可见:CopyOnWriteArrayList内部实际上就是维护了一个被 volatile 修饰的数组,来保证数据的内存可见性。

2.1、get() 方法 - 源码

final Object[] getArray() {
    return this.array;
}

private E get(Object[] var1, int var2) {
     return var1[var2];
}

public E get(int var1) {
     return this.get(this.getArray(), var1);
}

上述源码可见,其获取元素的操作逻辑清晰:

  1. get() “读操作”是没有加锁,直接读取,效率是最高的

2.2、add() 方法 - 源码

 public boolean add(E var1) {
 		// 使用lock,保证线程安全,写线程时同一时刻只有一个
        ReentrantLock var2 = this.lock;
        var2.lock();

        boolean var6;
        try {
           //获取旧数据的组引用
            Object[] var3 = this.getArray();
            int var4 = var3.length;
            //拷贝原容器,长度位原容器的长度+1
            Object[] var5 = Arrays.copyOf(var3, var4 + 1);
            //在新容器中添加新的数据
            ar5[var4] = var1;
            //将旧容器引用指向新的容器
            this.setArray(var5);
            var6 = true;
        } finally {
           //解锁
            var2.unlock();
        }

        return var6;
    }

上述源码可见,其添加元素的操作逻辑清晰:

  1. 采用 ReentrantLock 保证同一时刻只有一个写线程执行;
  2. 创建新的容器(长度为:旧容器长度+1),把旧容器copy过来
  3. 在新容器进行写的操作,在进行新旧容器引用的切换
  4. 在 finally 块中释放锁,以便其他线程可以访问

2.3、remove() 方法 - 源码

public E remove(int var1) {
		//加锁
       ReentrantLock var2 = this.lock;
       var2.lock();

       Object var11;
       try {
           //获取容器内容
           Object[] var3 = this.getArray();
           int var4 = var3.length;
           Object var5 = this.get(var3, var1);
           int var6 = var4 - var1 - 1;
           if (var6 == 0) {
               //如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用
               this.setArray(Arrays.copyOf(var3, var4 - 1));
           } else {
              否则,将除要删除元素之外的其他元素拷贝到新副本中,并切换引用
               Object[] var7 = new Object[var4 - 1];
               System.arraycopy(var3, 0, var7, 0, var1);
               System.arraycopy(var3, var1 + 1, var7, var1, var6);
               this.setArray(var7);
           }

           var11 = var5;
       } finally {
           //释放锁
           var2.unlock();
       }

       return var11;
   }

上述源码可见,其删除元素的操作逻辑清晰:

  1. 采用 ReentrantLock 保证同一时刻只有一个删除线程执行
  2. 将 remove 元素之外的其他元素拷贝到新副本中,然后切换引用,再将原容器的引用指向新的副本
  3. 在 finally 块中释放锁,以便其他线程可以访问

三、CopyOnWriteArraySet

1、介绍

CopyOnWriteArraySet 是一个线程安全的集合容器,它使用了“写时复制”的技术来实现,并且具有可读性和数据不变性的特点。虽然它的写性能较差,但在读多写少的场景中,CopyOnWriteArraySet 能够提供更好的性能和可靠性。

其优缺点同 CopyOnWriteArrayList 一样

2、原理【源码分析】

CopyOnWriteArraySet 实现了 Set 接口,并且使用了 CopyOnWriteArrayList 来作为底层的数据结构,因此其内部方法的原理同CopyOnWriteArrayList 是一样的,可自行翻看源码查看,在此处则不进行分析了…

  • 24
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值