首页 > 编程语言 >【Java 并发】【十】【JUC数据结构】【一】CopyOnWriteArrayList原理

【Java 并发】【十】【JUC数据结构】【一】CopyOnWriteArrayList原理

时间:2023-04-08 23:55:57浏览次数:47  
标签:JUC elements Java index lock CopyOnWriteArrayList 修改 线程 数组

1  前言

我们前面看过了volatile、synchronized以及AQS的底层原理,以及基于AQS之上构建的各种并发工具,ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier,那么我们这节该看什么了,是不是要看运用了。在日常的业务编程中经常使用到的内存数据结构有:Map、Set、List、Queue系列的数据结构,对应非线程安全的数据结构有,HashMap、HashSet、ArrayList、LinkedList、ArrayDeque等。这些内存的数据结构在多线程同时进行读写操作下并不是线程安全的,而JUC包下为我们提供了很多线程安全的Map、Set、List、Queue类型的数据结构。包括CopyOnWriteArrayList、ConcurrentLinkedQueue、ConcurrentHashMap、LinkedBlockingQueue、ArrayBlockingQueue、DelayQueue、SynchronousQueue等。这些并发安全的数据结构,就是我们接下来要看的。

 这节我们就来看看CopyOnWriteArrayList的原理。

2  内部属性

先来看看CopyOnWriteArrayList,内部有哪些属性:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    // 这个互斥可重入锁,用来保证多线程并发修改时候的线程安全
    final transient ReentrantLock lock = new ReentrantLock();

    // 使用volatile修饰的Object数组,数组是用来保存数据的
    // 而volatile可以保证可见性和有序性,一旦array这个引用发生改变,其它线程立即可见
    private transient volatile Object[] array;
    
    // 获取保存数据的底层数组引用
    final Object[] getArray() {
        return array;
    }
    // 设置保存数据的底层数组
    final void setArray(Object[] a) {
        array = a;
    }
}

上面就是CopyOnWriteArrayList 这个数据结构内部的属性,只有一个保存数据的Object数组,还有一个保证线程安全的ReentrantLock锁。接下来我们看看具体的方法动作是如何来保证并发的。

3  set方法

我们首先看一下set方法的内部实现,看看是怎么保证线程安全的。具体源码如下图所示:

public E set(int index, E element) {
    // 这里首先获取内部的一个互斥锁
    final ReentrantLock lock = this.lock;
    // 然后对互斥锁进行加锁,保证同一时间只有一个线程能对数据进行修改
    lock.lock();
    try {
        // 获取内部保存数据的数组
        Object[] elements = getArray();
        // 根据index索引,获取修改前这个数组下标保存的旧值
        E oldValue = get(elements, index);
        // 对比当前值和要修改后的值,如果不等则需要进行修改。
        if (oldValue != element) {
            // 获取当前数组的长度
            int len = elements.length;
            // 注意这里,新复制一个新的数组newElements,数组内元素的值跟elements一样,长度一样。
            Object[] newElements = Arrays.copyOf(elements, len);
            // 为新数组的index进行赋值,修改为新的值
            newElements[index] = element;
            // 注意:这里将新的newElements数组赋值给CopyOnWriteArrayList内的array引用
            // 由于使用了volatile修饰,保证可见性,所以这里一旦赋值成功,其它线程立马可见
            setArray(newElements);
        } else {
            // 不做任何变化,还是赋值回原来的数组
            setArray(elements);
        }
        // 返回修改前的旧值
        return oldValue;
    } finally {
        // 释放锁,让其它线程可以操作
        lock.unlock();
    }
}

上面的图的关键逻辑可以总结为以下几步:
(1)每次修改前都需要进行加锁,调用ReentrantLock.lock方法进行加锁,保证同一时间只有一个线程能进行修改操作
(2)获取内部保存数据的数组elements,直接根据要修改第几个元素获取到当前值oldValue;对比当前的值oldValue和要修改后的值element;如果oldValue == element,说明修改前后的值一样,无需进行修改了
(3)如果oldValue != element,这个时候则调用Arrays.of 方法复制一份一模一样的数据出来,修改新的数组的index位置的值为element(假如此时有别的线程在读取旧的数组元素,则是不影响的)
(4)修改完成之后,将新数组newElements赋值给引用array,由于array引用使用了volatile修饰,保证了可见性。所以一旦array引用指向的内存地址发生了改变,其它线程立马可见,之后使用的都是新的数组。

我们画个图来理解一下:

大致意思就是进行修改操作的时候:
(1)先进行加锁,如果修改的不相等然后复制一份一模一样的数据出来,此时内存里面就存在两份快照数据;这个时候要对新的数组元素进行修改,旧的不改变。
(2)当新的内存快照数据修改完成之后,就把调用setArray()方法,将数组的引用指向新的内存快照,由于使用了volatile修饰,一旦引用发生改变,其它线程立即可见,操作的就是新的数组了。

4  get方法

接下来我们继续,看一下get()方法的内部源码:

public E get(int index) {
    return get(getArray(), index);
}
// 直接根据返回的数组,以及获取数组元素的下标,直接取出
private E get(Object[] a, int index) {
    return (E) a[index];
}

上面就是get方法的流程,与set方法不一样的是,这里get的时候无需进行加锁,就是直接取出数组某个位置的元素即可。

接下来,我们以4个线程为例(2个写2个读),画个图分析一下set和get多线程并发的场景,分析一下内部是怎么读取数据和修改数据的,图中的数字序号就表示发生的先后顺序。

(1)线程1执行set方法,先调用ReentrantLock.lock方法进行加锁,这个时候线程2也调用ReentrantLock.lock方法,但是发现别人已经加锁了,这个时候线程2就会被阻塞住。
(2)线程1执行set(2,7)方法,要修改第二位的元素数据为7,复制出一个新的数组。但是同时线程3执行get(2)方法,由于数组的引用array指向的还是数组1,所以此时得到的值是5。
(3)线程1对复制出来的新数组进行修改,设置第2个元素的值为7,同时setArray(数组2)方法,将数组的引用array指向数组2。
(4)此时线程4执行get(2)方法,由于此时array引用已经指向了数组2,所以获取的是数组2的第2个元素,返回7
(5)线程1释放锁,返回结果
我们可以看到线程1在修改数据期间,线程3、线程4读取到的数据不一样,这样有问题吗?这个就是CopyOnWrite类型数据结构的特点了,这种数据结构其实就是牺牲了数据的强一致性,来换取读高并发。
每次修改的时候,都需要进行加锁,然后复制出一个副本出来进行修改,同时读数据操作不需要进行加锁,不会有任何阻塞;
一旦修改完成,将新的副本赋值给引用的时候,别的线程读取的时候就会立即读到新的副本内容,相当于是数据弱一致性的,牺牲了强一致性来换取读取的并发能力。
同时由于每次修改都要复制出来一份新的数据,空间开销很大;这种数据结构只适合在读多写少,同时要保证线程安全,不追求强一致性的情况。
它本身就是这么设计的,牺牲了内存空间、牺牲了数据的强一致性,来换取读取的并发提升。

5  add方法

接下来我们继续,看一下add()方法的内部源码:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 获取旧数组的引用
        Object[] elements = getArray();
        // 获取旧数组的长度
        int len = elements.length;
        // 注意这里复制出一个新数组,新数组长度为旧数组长度 + 1
        // 同时把旧数组0~length-1位的元素复制到新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 新数组的length位元素存储新增的值e
        newElements[len] = e;
        // 新数组地址赋值给引用array
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

6  remove方法

接下来我们继续,看一下remove()方法的内部源码:

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;
        // 如果numMove为0,表示要删除的元素在数组的最后一位
        if (numMoved == 0)
            // 要删除的元素在数组的最后一位,直接将0~len-2位复制跟新数组即可
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            // 新数组长度为len-1
            Object[] newElements = new Object[len - 1];
            // 旧数组的0~index-1位元素复制给新数组
            System.arraycopy(elements, 0, newElements, 0, index);
            // 旧数组的index + 1 ~ len -1 位元素复制给新数组
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            // 将新数组的地址赋值给引用
            setArray(newElements);
        }
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

上面的两个add、remove方法跟之前画图讲解的set方法源码原理基本是一样的,都是修改数据前进行加锁、然后复制出一个新的数组进行操作。不同的只是复制的元素长度不一样而已,这里就不再画图讲解了。
add方法只是复制出一个新的数组,长度比旧数组多1,然后在数组末尾放入这个新的元素。
remove方法这里就是将0~index -1、index + 1 ~ len -1 位置的元素复制给新数组而已。

7  小结

好了,本节我们CopyOnWriteArrayList就了解到这里就结束了哈,有理解不对的地方欢迎指正哈。

标签:JUC,elements,Java,index,lock,CopyOnWriteArrayList,修改,线程,数组
From: https://www.cnblogs.com/kukuxjx/p/17299437.html

相关文章

  • JavaWeb-jsp-19课-JSP语法-2023-04-08
    <%@pagecontentType="text/html;charset=UTF-8"language="java"%><html><head><title>$Title$</title></head><body><%--注释JSP带百分号--%><%=newjava.util.Date()%>&l......
  • Java: Course1
    面向对象和面向过程的区别:面向过程:(如C语言)根据需要分析步骤,将这些函数根据步骤叠加起来,一个套着一个,一直到需求全部实现,如果需求在代码实现之后变更,几乎需要全部重新进行程序设计,代码的维护性比较差.类似炒饭,将所有的原料全部混合,不容易单独的删除其中某个部分.面向对......
  • Java: Course2
    一.static的使用:1.修饰变量:(静态变量)可以在对象之间共享值2.修饰函数(静态函数)被static修饰过的函数只能调用被static修饰过的变量或方法,不能以任何方式引用this或super。被static修饰的变量或函数也可以添加权限修饰符(private,public..)。此外,static方法必须被实现,不......
  • java -- 异常处理、Collection、Iterator迭代器、泛型
    异常处理Java异常处理的五个关键字:try、catch、finally、throw、throws抛出异常throw在编写程序时,我们必须要考虑程序出现问题的情况当调用方法使用接受到的参数时,首先需要先对参数数据进行合法的判断,数据若不合法,就应该告诉调用者,传递合法的数据进来。这时需要使用抛出异常的......
  • windows下无法访问静态图片,报错java.net.UnknownHostException
    如果你是这种方式映射静态文件的,那可以继续看下去了@Value("${prop.me.hddir}")privateStringhddir;privatevoidaddstatic(ResourceHandlerRegistryregistry){Stringdir=hddir+"/static/";log.info("启动加资源路径...addResourceHa......
  • Java笔记(14) UDP通讯程序Demo
    实现一个简单的UDP通信程序,仅作为笔记使用网络编程中有三要素:IP、端口号和通信协议,分别用来确定对方在互联网上的地址、指定接受数据的软件和确定数据在网络中传输的规则。IP地址IP地址分为IPv4地址和IPv6地址,这里不做讨论。IPv4地址中分为公网地址(万维网使用)和私有地址(局......
  • Day02 - JavaScript + CSS Clock
    Day02-JavaScript+CSSClock作者:©liyuechun简介第二天的练习是用JS+CSS模拟时钟效果。效果如下:实现以上模拟时钟的效果,大致思路和解决方案如下:分别获取到当前时间的时、分、秒。通过时分秒对一圈360度,进行映射,确定每一个指针所需旋转的角度。通过CSS的transform:rotate(deg),......
  • JavaScript 中使用正则表达式的方法
    目录使用方法常见的使用方法test()exec()match()replace()split()使用方法在JavaScript中,正则表达式可以用字面量语法创建。字面量语法是一种非常简单直观的表示正则表达式的方式。它使用两个斜杠(/)括起来,如下所示:constregex=/pattern/;例如,要匹配字母a和b之间的所有字......
  • Java的jar包依赖版本冲突解决
    参考:https://blog.csdn.net/weixin_38898423/article/details/128178055https://blog.csdn.net/Yal_insist/article/details/127669985一、依赖冲突不同的第三方jar包可能同时对某一个jar包的不同版本含有依赖,但是Maven解析pom.xml文件时,同一个jar包只会保留一个Maven默认处理策略......
  • 【Java】数组
    数组是编程语言中常见的数据结构,用来存储一组相同数据类型的数据,可以通过整型索引访问数组中的每一个值。需要注意,同一个数组中存储的所有元素的数据类型必须相同。根据数组存放元素的组织结构,可将数组分为一维数组、二维数组以及多维(三维及以上)。创建数组:data_type[]varName;......