首页 > 其他分享 >CopyOnWriteArrayList与CopyOnWriteArraySet详解

CopyOnWriteArrayList与CopyOnWriteArraySet详解

时间:2022-10-23 23:26:11浏览次数:53  
标签:index elements lock Object newElements CopyOnWriteArrayList 详解 len CopyOnWriteAr

什么是CopyOnWrite容器

  【1】CopyOnWrite容器是基于并发模式Copy-on-Write模式(最简单的并发解决方案)实现的用于避免共享的数据集合。

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

  【3】适用场景:读多写少的场景。

 

源码分析CopyOnWriteArrayList的实现

  【1】属性说明

//用于锁住所有变化情况
final transient ReentrantLock lock = new ReentrantLock();

//存储数据的数组只能通过getArray/setArray进行改变
private transient volatile Object[] array;

 

  【2】方法解析(仅展示部分方法)

    1)添加方法

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 上锁,只允许一个线程进入
    lock.lock();
    try {
        // 获得当前数组对象
        Object[] elements = getArray();
        int len = elements.length;
        // 拷贝到一个新的数组中
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 插入数据元素
        newElements[len] = e;
        // 将新的数组对象设置回去
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len);
        Object[] newElements;
        int numMoved = len - index;
        if (numMoved == 0)
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            newElements = new Object[len + 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index, newElements, index + 1, numMoved);
        }
        newElements[index] = element;
        setArray(newElements);
    } finally {
        lock.unlock();
    }
}

    2)设置方法

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        E oldValue = get(elements, index);

        if (oldValue != element) {
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
        } else {
            // 这里其实是将副本,重新放回去
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

 

    3)删除方法

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

 

    4)获取方法

private E get(Object[] a, int index) {
    return (E) a[index];
}

public E get(int index) {
    return get(getArray(), index);
}

//final修饰方法之后该方法无法被子类覆盖
final Object[] getArray() {
    return array;
}

 

  【3】汇总说明

    1.CopyOnWriteArrayList之所以选择数组而不是链表作为变量的存储空间的原因:

      1)提高处理速度,因为数组存储在内存中一块连续的空间,而链表则是分散的,采用Arrays.copyOf 本质上底层还是使用 System.arraycopy 将那块连续的内存空间的数据一次性拷贝,减少操作次数。

    2.由源码可以看到,每次进行修改的时候都会加锁仅限于一个线程进行变更操作,避免了共享变量并发写的问题。所以是线程安全的。

    3.但是其占用内存空间容易出现问题,如:在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。而Full GC过长则应用响应时间也随之变长。

    4.数据一致性问题,我们可以看出数据并不是实时一致性的,而是最终一致性。因为会先将数据拷贝到newElements 中,再设置到array的指针指向。要知道操作系统是基于时间片轮转机制分配运行时间(如:时间耗尽没有新的时间片给予,会导致线程上下文切换),所以中间的间隔时间可以假设很长,那么修改是写入了,但是变更还没进行。其次,在加锁的时间内,其他线程读取的其实都是没有修改的数据。

    

 

源码分析CopyOnWriteArraySet的实现

  【1】属性说明

private final CopyOnWriteArrayList<E> al;

  【2】方法说明

public boolean add(E e) {
    return al.addIfAbsent(e);
}

//CopyOnWriteArrayList类的方法
public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false : addIfAbsent(e, snapshot);
}

private static int indexOf(Object o, Object[] elements, int index, int fence) {
    if (o == null) {
        for (int i = index; i < fence; i++)
            if (elements[i] == null)
                return i;
    } else {
        for (int i = index; i < fence; i++)
            if (o.equals(elements[i]))
                return i;
    }
    return -1;
}

private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) {
            // Optimize for lost race to another addXXX operation
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;
            if (indexOf(e, current, common, len) >= 0)
                    return false;
        }
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

 

  【3】汇总说明

    1.CopyOnWriteArraySet的实现严格来说是基于CopyOnWriteArrayList进行实现的,去重逻辑在add中体现。

    2.其次是效率问题:每次插入都需要去遍历CopyOnWriteArrayList数组一次。

    3.虽然也是线程安全的,但是CopyOnWriteArrayList的缺点全部都会继承。

 

标签:index,elements,lock,Object,newElements,CopyOnWriteArrayList,详解,len,CopyOnWriteAr
From: https://www.cnblogs.com/chafry/p/16770916.html

相关文章

  • 邮件协议详解
    邮件的发送和接收过程——STMP、POP、IMAP、MIME电子邮件发送协议是一种基于“推”的协议,主要包括SMTP;邮件接收协议则是一种基于“拉”的协议,主要包括POP协议和......
  • python中format的详解
    format是字符串内嵌的一个方法,用于格式化字符串。以大括号{}来标明被替换的字符串。它通过{}和:来代替%。1、基本用法1.按照{}的顺序依次匹配括号中的值s="{}isa{}......
  • SpringMVC框架详解
    简述SpringMVC是一种基于Java的实现MVC设计模型的请求驱动类型的轻量级Web框架,属于Spring FrameWork的后续产品,已经融合在SpringWebFlow里面。Spring......
  • php-fpm 配置详解
    php-fpm工作流程php-fpm全名是PHPFastCGI进程管理器php-fpm启动后会先读php.ini,然后再读相应的conf配置文件,conf配置可以覆盖php.ini的配置。启动php-fpm之后,会创建一......
  • Hibernate缓存及核心接口类详解
    Hibernate缓存概述一级缓存(session级别缓存)也叫事务级别的缓存二级缓存(sessionFactory缓存)也叫应用级缓存三级缓存(查询缓存)区别:一级缓存的生命周期和session的生命......
  • 10.stm32启动文件详解
    ......
  • 7.Python自定义排序详解
    如果以创建的对象作为列表中的元素,那么对列表进行排序时可使用sort()函数或sorted()函数,但要注意的是:①当排序对象为列表的时候两者适合的场景不同②sorted()函数会返......
  • Sentinel(二)--限流详解
    在Sentinel中,限流的直接表现形式是,在执行EntrynodeA=SphU.entry(resourceName)的时候抛出FlowException异常。FlowException是BlockException的子类,您可以捕捉Blo......
  • php yield详解
       简单例子//包含yield的函数可以生成一个generator对象,可以被foreach遍历functionGenerator(){for($i=0;$i<3;$i++){echo"输出存在......
  • JDBC各个类详解_ResultSet_基本使用与JDBC各个类详解_ResultSet_遍历结果集
    JDBC各个类详解_ResultSet_基本使用ResultSet:结果集对象,封装查询的结果next():游标向下移动一行......