Java中的CopyOnWriteArrayList原理详解

 更新时间:2023年12月27日 10:58:10   作者:笑我归无处  
这篇文章主要介绍了Java中的CopyOnWriteArrayList原理详解,如源码所示,CopyOnWriteArrayList和ArrayList一样,都在内部维护了一个数组,操作CopyOnWriteArrayList其实就是在操作内部的数组,需要的朋友可以参考下

CopyOnWriteArrayList的原理是什么

CopyOnWriteArrayList是线程安全版本的ArrayList。

这里先上一小段源码

final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
/**
 * Gets the array.  Non-private so as to also be accessible
 * from CopyOnWriteArraySet class.
 */
final Object[] getArray() {
    return array;
}
/**
 * Sets the array.
 */
final void setArray(Object[] a) {
    array = a;
}
/**
 * Creates an empty list.
 */
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

如源码所示,CopyOnWriteArrayList和ArrayList一样,都在内部维护了一个数组。操作CopyOnWriteArrayList其实就是在操作内部的数组。

但关键是和ArrayList的不同之处

1) 使用volatile修饰内部数组

private transient volatile Object[] array;

看这行代码,使用volatile修饰了内部数组 volatile关键字保证了每次拿到的内部数组都是最新值。因为volatile关键字表示直接去主存中获取值,因此哪怕别的线程刚修改完内部数组,也能保证获取内部数组时是最新的。

2) 加锁

提到并发编程,当然少不了加锁。

final transient ReentrantLock lock = new ReentrantLock();

CopyOnWriteArrayList每创建一个实例,都会同时创建一个ReentrantLock锁。 CopyOnWriteArrayList会在增,删,改操作时添加锁,而不会在读操作时加锁。

3) 使用COW思想操作数组。

COW即是CopyOnWrite的缩写。即每次在写入之前,先获取源数据的拷贝,修改完拷贝后,再保存到源数据中。

get操作

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

如源码所示,get操作没有加锁,直接返回数组中的对象。

set操作

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 {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

大致流程:

  • 加锁
  • 获取源数组
  • 判断新值和旧值是否相同
  • 不同的话拷贝源数组,更新值,然后更新源数组
  • 相同的话,不更新值,然后更新源数组(数组内容没变)
  • 释放锁

add操作

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();
    }
}

大致流程:

  • 加锁
  • 获取源数组
  • 复制一个源数组长度+1的新数组
  • 在数组末尾赋值,然后更新源数组
  • 释放锁

remove操作

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elemnts, 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();
    }
}

大致流程:

  • 加锁
  • 获取源数组
  • 判断删除的位置是不是数组末尾
  • 是末尾的话,复制一个数组长度减一的数组,然后更新源数组
  • 不是末尾的话,做成一个不包含需要删除的元素的新数组,然后更新源数组
  • 释放锁

以上操作可以看出, 查操作不加锁 增删改操作大致流程都是一样的,先加锁,然后复制一份源数组,操作完后写入源数组,释放锁。

那么为什么要这么做呢?都已经加锁了,为什么不能直接操作源数组呢?不然加锁是为了什么? 这是我第一次看到这种做法时的疑问。接下来一一解释。

读操作为什么不加锁

当然是为了提高读操作的效率啦

既然加锁了为什么不能直接操作源数组?

因为读操作没有加锁。增删改操作时,读操作可以在任何一步时获取数组里的值。 如果刚生成一个新数组,还没有更新里面的值的情况下就被执行了读操作,就会出现不可预料的情况。

因此为了保证数据的最终一致性。只有当数组完全更新结束后,再刷新源数组的值,才能保证读取的要么是旧值,要么是最新值。

既然使用COW就可以保证读操作不出现异常,那为什么还要加锁?

加锁是为了保证和其他写操作不冲突。

CopyOnWriteArrayList的优缺点

优点: 在保证线程安全的情况下,可以获得非常高效的读操作。 虽然写操作性能低下,但能保证线程安全。

缺点: 因为每次写操作都需要复制一份新数组,所以写操作性能低下,尤其是数组长度很长时,不建议使用CopyOnWriteArrayList。

CopyOnWriteArrayList的应用场景

高并发场景,多读取,少写入。

到此这篇关于Java中的CopyOnWriteArrayList原理详解的文章就介绍到这了,更多相关CopyOnWriteArrayList原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringCloud之熔断监控Hystrix Dashboard的实现

    SpringCloud之熔断监控Hystrix Dashboard的实现

    这篇文章主要介绍了SpringCloud之熔断监控Hystrix Dashboard的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • Spring boot通过AOP防止API重复请求代码实例

    Spring boot通过AOP防止API重复请求代码实例

    这篇文章主要介绍了Spring boot通过AOP防止API重复请求代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-12-12
  • Springboot整合PageOffice 实现word在线编辑保存功能

    Springboot整合PageOffice 实现word在线编辑保存功能

    这篇文章主要介绍了Springboot整合PageOffice 实现word在线编辑保存,本文以Samples5 为示例文件结合示例代码给大家详细介绍,需要的朋友可以参考下
    2021-08-08
  • 简单阐述一下Java集合的概要

    简单阐述一下Java集合的概要

    今天给大家带来的文章是关于Java的相关知识,文章围绕着Java集合的概要展开,文中有非常详细的介绍及代码示例,需要的朋友可以参考下
    2021-06-06
  • Java中invokedynamic字节码指令问题

    Java中invokedynamic字节码指令问题

    这篇文章主要介绍了Java中invokedynamic字节码指令问题,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-04-04
  • java中BigDecimal进行加减乘除的基本用法

    java中BigDecimal进行加减乘除的基本用法

    大家应该对于不需要任何准确计算精度的数字可以直接使用float或double运算,但是如果需要精确计算的结果,则必须使用BigDecimal类,而且使用BigDecimal类也可以进行大数的操作。下面这篇文章就给大家介绍介绍关于java中BigDecimal进行加减乘除的基本用法。
    2016-12-12
  • Selenium Webdriver实现截图功能的示例

    Selenium Webdriver实现截图功能的示例

    今天小编就为大家分享一篇Selenium Webdriver实现截图功能的示例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-05-05
  • java保留小数的四种实现方法

    java保留小数的四种实现方法

    这篇文章主要为大家详细介绍了java保留小数的四种实现方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-11-11
  • Java分别利用深度优先和广度优先求解迷宫路径

    Java分别利用深度优先和广度优先求解迷宫路径

    这篇文章主要为大家详细介绍了Java如何利用深度优先的非递归遍历方法和广度优先的遍历方法实现求解迷宫路径,文中的示例代码讲解详细,需要的可以参考一下
    2022-08-08
  • java动态目录树的实现示例

    java动态目录树的实现示例

    在开发过程中,常常需要对目录结构进行操作和展示,本文主要介绍了java动态目录树的实现示例,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03

最新评论