初探 JUC 并发编程:Java 并发包中并发 List 源码剖析

最近在阅读 《Java 并发编程之美》这本书,感觉学到了很多东西;所以我决定将从事书中学到的思想和一些经典的案例整理成博客的形式与大家分享和交流,如果对大家有帮助别忘了留下点赞和关注捏。

第五部分:Java 并发包中并发 List 源码剖析

5.1)介绍

并发包中的并发 List 只有 CopyOnWriteArrayList。这是一个线程安全的 ArrayList,对其进行修改的操作都是在底层的一个复制数组上进行的,也就是使用了写时复制策略。

在类中有一个 array 数组对象用来存放具体的元素:

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

每个 CopyOnWriteArrayList 都有一个对象锁来保证只有一个线程对 array 进行修改操作:

    /**
     * The lock protecting all mutators.  (We have a mild preference
     * for builtin monitors over ReentrantLock when either will do.)
     */
    final transient Object lock = new Object();

上面的注解的含义是这样的:
保护所有变化的锁。(我们对内置监控器略有偏好,而不是 ReentrantLock ,两者都可以。)
这个锁的类型要根据不同的 Java 源码来看,不同的团队会有不同的设计选择,所以如果追源码发现这个锁的实现不同,是因为我们看的源码的版本不同。

比如来看一个其他版本的 Java 源码:

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

这里使用的就是 ReentrantLock 独占锁。

5.2)主要方法源码剖析

5.2.3)初始化方法

首先来看较为简单的无参构造方法,当调用这个构造方法的时候,会给 array 数组赋一个大小为 0 的 Object 数组。

    /**
     * Creates an empty list.
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
    
		/**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }

除了有参构造方法, CopyOnWriteArrayList 类还提供了这些构造方法:

在这里插入图片描述


    /**
     * Creates a list holding a copy of the given array.
     *
     * @param toCopyIn the array (a copy of this array is used as the
     *        internal array)
     * @throws NullPointerException if the specified array is null
     */
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
    
    // 复制方法
		public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

上面展示的是传入对象数组的构造方法,方法中会调用 Arrays.copyOf() 方法来创建一个复制数组,然后将其赋值给 array 变量。


    /**
     * Creates a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection of initially held elements
     * @throws NullPointerException if the specified collection is null
     */
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
        // 如果传入数组的类型恰好就是 CopyOnWriteArrayList
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
        // 对于其他类型就执行复制的逻辑
            elements = c.toArray();
            if (c.getClass() != java.util.ArrayList.class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }

最后一个就是传入 Collection 实现类的构造方法,首先判断 List 类型是否为 CopyOnWriteArrayList 如果是的话就直接将其中的 array 赋值给这个 CopyOnWriteArrayList ,如果是其他的类型就将其转化为 Array 然后执行 copyOf 操作。

上面的构造方法中,如果发现传入的类是 CopyOnWriteArrayList 就将它底层的数组直接赋值给这个新的对象的 array,这不会导致它们操纵的是一个数组吗?
其实如果不进行任何操作(比如增加元素或者删除元素),这两个对象的底层确实就是同一个数组;但是如果执行了新增或者删除的操作,类中的 array 的长度和其中含有元素的长度是相同的,也就是如果新增或者删除元素就会引发数组的更新,此时会将原本的数组复制到一个新的指定长度的数组中,此时两个对象操控的元素就不同了。

写个反射来验证一下:

public class Main {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        CopyOnWriteArrayList<Integer> integers1 = new CopyOnWriteArrayList<>();
        CopyOnWriteArrayList<Integer> integers2 = new CopyOnWriteArrayList<>(integers1);
        Class<? extends CopyOnWriteArrayList> aClass = integers1.getClass();
        Field array = aClass.getDeclaredField("array");
        array.setAccessible(true);
        Object[] arr1 = (Object[])array.get(integers1);
        Object[] arr2 = (Object[])array.get(integers2);
        System.out.println(arr1 == arr2);
    }
}

在上面的代码中,首先创造了一个 CopyOnWriteArrayList 对象 integers1,然后将其作为参数传递给新的 CopyOnWriteArrayList 对象,此时使用反射方法拿到它们的 array 属性,通过 == 检测是否是相同的数组:

在这里插入图片描述

最终发现输出的是 true,但是如果在代码中加一个 add()

public class Main {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        CopyOnWriteArrayList<Integer> integers1 = new CopyOnWriteArrayList<>();
        CopyOnWriteArrayList<Integer> integers2 = new CopyOnWriteArrayList<>(integers1);
        integers1.add(0); // 新增的代码
        Class<? extends CopyOnWriteArrayList> aClass = integers1.getClass();
        Field array = aClass.getDeclaredField("array");
        array.setAccessible(true);
        Object[] arr1 = (Object[])array.get(integers1);
        Object[] arr2 = (Object[])array.get(integers2);
        System.out.println(arr1 == arr2);
    }
}

此时再去运行,得到的就是 false 了
在这里插入图片描述

5.2.2)添加元素

CopyOnWriteArrayList 类中提供了许多添加元素的方法,有 add(E e) add(int index, E element) addifAbsent(E e) addAllAbsent(Collection<? extends E> c) 等等,但是他们的实现方式都差不多,这里以 add(E e) 方法举例来讲解,通过这个也可以更好的理解上面的解释。

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#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 的数组中。

5.2.3)获取指定位置的元素

    /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }
    
    /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }
    
    @SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

上面展示的三个方法就是 get(int index) 方法调用的几个方法,执行的逻辑就是先获取到 array 数组,然后取出指定位置的元素。

考虑一个问题,这个方法没有枷锁的,如果一个线程在执行的时候,另一个线程调用了 remove 方法,现在获取的元素删除掉导致出现问题吗?

remove 方法和上面的 add 方法的逻辑是相同的,就是将原本的数组复制到一个新的容量的数组中,然后这个数组赋值给 array,但是此时原本的数组并不会销毁,因为它正在被此时调用 get 方法的线程中引用,所以此时获取到的仍然是正确是元素。

5.2.4)修改指定的元素

    /**
     * Replaces the element at the specified position in this list with the
     * specified element.
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    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 {
                // 不完全是没有操作; 维护了 volatile 的写入语义
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock(); // 释放锁
        }
    }

修改操作和上面的 add 以及 remove 方法相同,都是首先复制一份数组,然后再新复制的数组上执行替换的操作,即使当发现 oldValue == element 的时候会执行这个操作:setArray(elements); 的操作,这是为了维护 volatile 的语义,因为 array 是一个被 volatile 关键字修饰的属性。

执行这个操作的目的是确保对 volatile 类型数组的写操作具有正确的语义。volatile 关键字用于声明变量,以确保多线程之间的可见性和一致性。

当一个线程修改了 volatile 变量的值时,这个值会立即被写回到主内存中,并且其他线程在读取这个变量时会从主内存中获取最新的值,而不是从自己的线程缓存中获取。

即使知道数组的内容没有变化,也还是要重新去设置 array。

5.2.5)删除元素

    /**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).  Returns the element that was removed from the list.
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        synchronized (lock) { // 获取独占锁
            Object[] es = getArray();
            int len = es.length;
            
            // 获取指定元素
            E oldValue = elementAt(es, index);
            
            // 准备需要使用的变量
            int numMoved = len - index - 1;
            Object[] newElements;
            
            if (numMoved == 0)
             // 如果要删除的是最后一个元素
                newElements = Arrays.copyOf(es, len - 1);
            else {
            // 删除的是其他元素,分两次将内容复制到新的数组
                newElements = new Object[len - 1];
                System.arraycopy(es, 0, newElements, 0, index);
                System.arraycopy(es, index + 1, newElements, index,
                                 numMoved);
            }
            setArray(newElements);
            return oldValue;
        }
    }

不用猜也可以知道,还是和上面的操作相同,操作仍然是在复制的数组中进行的,上面的代码中首先创建了一个长度为 len - 1 的新数组,然后将 0 到 index - 1 的元素复制到新的数组,再将 index + 1 到末尾的元素复制到新的数组,从而实现了删除的操作;因为除了读取以外的其他操作都需要获取锁,所以这里的删除不会被其他的操作影响到。

5.2.6)弱一致性的迭代器

说到迭代器,大家一定不会陌生,CopyOnWriteArrayList 类实现了 List 接口,所以其迭代器的操作逻辑和 ArrayList 等我们熟悉的集合是完全相同的:

    // 演示迭代器的使用
    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> integers = new CopyOnWriteArrayList<>();
        integers.add(1);
        integers.add(12);
        integers.add(123);
        Iterator<Integer> iterator = integers.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

弱一致性指的是,当返回一个迭代器的时候,其他线程对于 List 的增删改的操作对于这个迭代器是不可见的,下面来看一下具体的实现方式:

		// 获取迭代器										
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
    
    // 迭代器的初始化方法
		private COWIterator(Object[] elements, int initialCursor) {
	      cursor = initialCursor; // 初始指针
	      snapshot = elements; // 指向数组的指针
	  }

当调用 iterator 方法的时候,会返回一个 COWIterator 对象,这个对象的 snapshot 变量保存了当前 list 的内容,cursor 是遍历 list 时数据的下标。

5.3)总结

通过上面源码的阅读,可以发现, CopyOnWriteArrayList 类是通过写时复制的策略来保证 list 的一致性,修改时候的操作不是原子性的,所以在增删改的过程中都使用了独占锁来保证这些操作的原子性。另外,类中还提供了一个弱一致性的迭代器,从而保证获取迭代器之后,其他线程的修改是不可见的,迭代器遍历的数组是一个快照。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/583890.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32CubeMX+MDK通过I2S接口进行音频输入输出(全双工读写一个DMA回调)续-音质问题解决总结

一、前言 之前进行了STM32CubeMXMDK通过I2S接口进行音频输入输出&#xff08;全双工读写一个DMA回调&#xff09;的研究总结&#xff1a; https://juejin.cn/post/7339016190612881408#heading-34 后续音质问题解决了&#xff0c;目前测试下来48khz的双声道使用效果很好&…

基于uniapp+微信小程序的智能停车场管理小程序,附源码

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

护航智慧交通安全 | 聚铭精彩亮相2024交通科技创新及信创产品推广交流会

4月26日&#xff0c;石家庄希尔顿酒店内&#xff0c;河北省智能交通协会盛大举办2024年度交通科技创新及信创产品推广交流会。聚铭网络受邀参与&#xff0c;携旗下安全产品及解决方案精彩亮相&#xff0c;为智慧交通安全保驾护航。 为深化高速公路创新驱动发展战略&#xff0…

Java网址url工具类

功能描述 无需引入三方依赖文本匹配网址&#xff08;支持多个&#xff09;网址解析&#xff08;包括协议、主机、路径、参数等&#xff09; package com.qiangesoft.image.utils;import org.springframework.util.Assert; import org.springframework.util.CollectionUtils;i…

深度学习基础之《TensorFlow框架(16)—神经网络案例》

一、mnist手写数字识别 1、数据集介绍 mnist数据集是一个经典的数据集&#xff0c;其中包括70000个样本&#xff0c;包括60000个训练样本和10000个测试样本 2、下载地址&#xff1a;http://yann.lecun.com/exdb/mnist/ 3、文件说明 train-images-idx3-ubyte.gz: training s…

C#编程模式之装饰模式

创作背景&#xff1a;朋友们&#xff0c;我们继续C#编程模式的学习&#xff0c;本文我们将一起探讨装饰模式。装饰模式也是一种结构型设计模式&#xff0c;它允许你通过在运行时向对象添加额外的功能&#xff0c;从而动态的修改对象的行为。装饰模式本质上还是继承的一种替换方…

分享三款可以给pdf做批注的软件

PDF文件不像Word一样可以直接编辑更改&#xff0c;想要在PDF文件上进行编辑批注需要用到一些专业的软件&#xff0c;我自己常用的有三款&#xff0c;全都是官方专业正版的软件&#xff0c;功能丰富强大&#xff0c;使用起来非常方便&#xff01; 1.edge浏览器 这个浏览器不仅可…

爬虫实战-房天下(bengbu.zu.fang.com/)数据爬取

详细代码链接https://flowus.cn/hbzx/3c42674d-8e6f-42e3-a3f6-bc1258034676 import requests from lxml import etree #xpath解析库 def 源代码(url): cookies { global_cookie: xeqnmumh38dvpj96uzseftwdr20lvkwkfb9, otherid: b44a1837638234f1a0a15e…

JavaEE 初阶篇-深入了解特殊文件(Properties 属性文件、XML)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 Properties 属性文件概述 1.1 Properties 属性文件特性与作用 1.2 使用 Properties 把键值对数据写出到属性文件中 1.3 使用 Properties 读取属性文件里的键值对数…

《动手学深度学习(Pytorch版)》Task03:线性神经网络——4.29打卡

《动手学深度学习&#xff08;Pytorch版&#xff09;》Task03&#xff1a;线性神经网络 线性回归基本元素线性模型损失函数随机梯度下降 正态分布与平方损失 线性回归的从零开始实现读取数据集初始化模型参数定义模型定义损失函数定义优化算法训练 线性回归的简洁实现读取数据集…

c#创建新项目

确保已安装.NET Core SDK。&#xff08;visual studio installer中可安装&#xff09; cmd中先引用到文件夹目录下。 mkdir MyConsoleApp MyConsoleApp是项目文件夹的名字。 mkdir 是一个命令行工具&#xff0c;用于在文件系统中创建新的目录&#xff08;文件夹&#xff09;…

用OpenCV先去除边框线,以提升OCR准确率

在OpenCV的魔力下&#xff0c;我们如魔法师般巧妙地抹去表格的边框线&#xff0c;让文字如诗如画地跃然纸上。 首先&#xff0c;我们挥动魔杖&#xff0c;将五彩斑斓的图像转化为单一的灰度世界&#xff0c;如同将一幅绚丽的油画化为水墨画&#xff0c;通过cv2.cvtColor()函数的…

主机ping不通虚拟机/虚拟机ping不通主机/xhell连接不了虚拟机/win10或win11系统升级导致无法连接到虚拟机

解决方案 重置网卡 找虚拟机ip&#xff0c;第二个inet对应的就是虚拟机ip地址 xshell连接 参考: 主机ping不通虚拟机

认识认识DHCP

文章目录 认识认识DHCP一、什么是DHCP&#xff1f;1.1、为什么要使用DHCP&#xff1f;1.2、DHCP是怎么工作的&#xff1f;1.2.1、客户端首次接入网络的工作原理1.2.2、客户端重用曾经使用过的地址的工作原理1.2.3、客户端更新租期的工作原理 二、配置DHCP Server&#xff0c;为…

电子式汽车机油压力传感器的接线方法及特点

电子式机油压力传感器由厚膜压力传感器芯片、信号处理电路、外壳、固定电路板装置和两根引线&#xff08;信号线和报警线&#xff09;组成。信号处理电路由电源电路、传感器补偿电路、调零电路、电压放大电路、电流放大电路、滤波电路和报警电路组成。 厚膜压力传感器是20世纪…

【UE5】动态播放媒体

最近项目中有一个需求&#xff0c;需要将场景中的42块屏幕都显示媒体内容&#xff0c;想着如果每一块屏幕都创建一个MediaPlayer资产、一个MediaSource资产、一个MediaTexture资产及创建对应的Material&#xff0c;就是4*42168个资产需要维护了&#xff0c;所以想着就全部采用动…

如何看待Agent AI智能体的未来

Agent AI智能体的未来 Agent AI智能体&#xff0c;也称为自主代理或智能代理&#xff0c;是指能够自主执行任务、与环境交互并作出决策的计算机程序或系统。这些智能体通常具备学习、适应和推理的能力&#xff0c;能够在复杂和不确定的环境中执行任务。随着技术的进步&#xf…

【OC和红移的双面材质】

OC和红移的双面材质 2021-12-23 18:36 rs oc 评论(0)

TiDB 利用binlog 恢复-反解析binlog

我们知道TiDB的binlog记录了所有已经执行成功的dml语句&#xff0c;类似mysql binlog row模式 &#xff0c;TiDB官方也提供了reparo可以进行解析binlog&#xff0c;如下所示: [2024/04/26 20:58:02.136 08:00] [INFO] [config.go:153] ["Parsed start TSO"] [ts449…

Linux网络抓包工具tcpdump是如何实现抓包的,在哪个位置抓包的?

Linux网络抓包工具tcpdump是如何实现抓包的&#xff0c;在哪个位置抓包的&#xff1f; 1. tcpdump抓包架构2. BPF介绍3. 从内核层面看tcpdump抓包流程3.1. 创建socket套接字3.2. 挂载BPF程序 4. 网络收包抓取5. 网络发包抓取6. 疑问和思考6.1 tcpdump抓包跟网卡、内核之间的顺序…