初探 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)// 如果传入数组的类型恰好就是 CopyOnWriteArrayListelements = ((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 的一致性,修改时候的操作不是原子性的,所以在增删改的过程中都使用了独占锁来保证这些操作的原子性。另外,类中还提供了一个弱一致性的迭代器,从而保证获取迭代器之后,其他线程的修改是不可见的,迭代器遍历的数组是一个快照。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

性能监控之prometheus+grafana搭建

前言 Prometheus和Grafana是两个流行的开源工具&#xff0c;用于监控和可视化系统和应用程序的性能指标。它们通常一起使用&#xff0c;提供了强大的监控和数据可视化功能。 Prometheus Prometheus是一种开源的系统监控和警报工具包。它最初由SoundCloud开发&#xff0c;并于…

Android log tag标签如am_pss意义

Android log tag标签如am_pss意义 Android输出日志中不同的标签代表不同的意义&#xff0c;比如 am_pss&#xff0c;则代表内存回收&#xff08;整理&#xff09;。定义在源代码文件 &#xff1a; https://android.googlesource.com/platform/frameworks/base//master/servic…

Python数据分析大作业(ARIMA 自回归积分滑动平均模型) 4000+字 图文分析文档 销售价格库存分析+完整python代码

资源地址&#xff1a;Python数据分析大作业 4000字 图文分析文档 销售分析 完整python代码 完整代码分析 ​ 同时销售量后1000的sku品类占比中&#xff08;不畅销产品&#xff09;如上&#xff0c;精品类产品占比第一&#xff0c;达到66.7%&#xff0c;其次是香化类产品&#x…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-8.1

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

使用opencv改变图片大小

使用opencv改变图片大小 图片的宽度和高度效果代码 图片的宽度和高度 宽度&#xff1a;图片的宽度指的是图像从左边缘到右边缘的水平跨度。在数字图像中&#xff0c;宽度通常是以像素&#xff08;pixels&#xff09;为单位来度量的。高度&#xff1a;图片的高度指的是图像从上…

【go项目01_学习记录day01】

博客系统 1 vscode开发go项目插件推荐1.1 CtrlShiftP&#xff08;俗称万能键&#xff09; &#xff1a;打开命令面板。在打开的输入框内&#xff0c;可以输入任何命令。1.2 开发时&#xff0c;我们需要经常查阅 Go 语言官方文档&#xff0c;可惜因国内访问外网不稳定&#xff0…

[Java EE] 多线程(五):单例模式与阻塞队列

1. 单例模式 单例模式是校招中最长考的设计模式之一,首先我们来谈一谈什么是设计模式: 设计模式就好像象棋中的棋谱一样,如果红方走了什么样的局势,黑方就有一定地固定地套路,来应对这样的局势,按照固定地套路来,可以保证在该局势下不会吃亏. 软件开发也是同样的道理,有很多…

Linux基础-socket详解、TCP/UDP

文章目录 一、Socket 介绍二、Socket 通信模型三、Socket 常用函数1 创建套接字2 绑定套接字3、监听连接4、接受连接5、接收和发送数据接收数据发送数据 6、关闭套接字 四、Socket编程试验1、源码server.cclient.c 2、编译&#xff1a;3、执行结果 五、补充TCP和UDP协议的Socke…

OpenAI 新推出 AI 问答搜索引擎——SearchGPT 震撼登场

您的浏览器不支持 video 标签。 OpenAI-SearchGPT 近日&#xff0c;OpenAI 曝光了自己的一款令人瞩目的 AI 问答搜索引擎——SearchGPT。这款搜索引擎带来了全新的搜索体验&#xff0c;给整个行业带来了巨大的压力。 SearchGPT 支持多种强大的功能。首先&#xff0c;它能够通过…

分布式与一致性协议之Raft算法(三)

Raft算法 如何复制日志 你可以把Raft算法的日志复制理解成一个优化后的二阶段提交(将二阶段优化成了一阶段)。优化后减少了一半的往返消息&#xff0c;也就是降低了一半的消息延迟&#xff0c;那日志复制的具体过程又是什么呢&#xff1f; 首先&#xff0c;领导者进入第一阶段…

【Redis 开发】多级缓存,本地进程缓存Caffeine

多级缓存 多级缓存本地进程缓存CaffeineCaffeine三种缓存驱逐策略 多级缓存 Redis处理并发的能力是非常强大的&#xff0c;但是tomcat的支持并发的能力跟不上Redis的性能&#xff0c;导致整体性能的下降 Redis缓存失效时&#xff0c;会对数据库产生冲击&#xff0c;之间再无屏…

LeetCode1017题:负二进制转换(原创)

【题目描述】 给你一个整数 n &#xff0c;以二进制字符串的形式返回该整数的 负二进制&#xff08;base -2&#xff09;表示。注意&#xff0c;除非字符串就是 "0"&#xff0c;否则返回的字符串中不能含有前导零。 示例 1&#xff1a; 输入&#xff1a;n 2 输出&…

基于Springboot的数字化农家乐管理平台(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的数字化农家乐管理平台&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系…

踏上R语言之旅:解锁数据世界的神秘密码(四)

文章目录 前言一、多元线性回归1.多元线性回归模型的建立2.多元线性回归模型的检验 二、多元线性相关分析1.矩阵相关分析2.复相关分析 三、回归变量的选择方法1.变量选择准则2.变量选择的常用准则3.逐步回归分析 总结 前言 回归分析研究的主要对象是客观事物变量间的统计关系。…

选择内核注意力 SK | Selective Kernel Networks

论文名称&#xff1a;《Selective Kernel Networks》 论文地址&#xff1a;https://arxiv.org/pdf/1903.06586.pdf 代码地址&#xff1a;https://github.com/implus/SKNet 在标准的卷积神经网络中&#xff0c;每层人工神经元的感受野被设计为具有相同的大小。神经科学界已经广…

使用OkHttp 缓存 API 调用提高Android应用性能

使用OkHttp 缓存 API 调用提高Android应用性能 坦率地说&#xff0c;我们都遇到过这样的情况——焦急地刷新应用&#xff0c;看着加载图标不停地旋转&#xff0c;等待那个至关重要的 API 响应。这样的等待我们已经是炉火纯青了&#xff0c;是吧&#xff1f;手指有节奏地轻敲屏…

《R语言与农业数据统计分析及建模》——多重共线性和逐步回归

一、多重共线性 多重共线性&#xff1a;在多元线性回归时&#xff0c;多个自变量之间存在高度相关关系&#xff0c;时模型估计失真或难以估计准确的情况。 一般地&#xff0c;多元线性回归中自变量间应尽量相互独立。常规模型诊断方法难以检测多重共线性。 1、案例解释 作物产…

ActiveMQ 反序列化漏洞 (CVE-2015-5254)

一、漏洞描述 Apache ActiveMQ 是由美国阿帕奇&#xff08;Apache&#xff09;软件基金会开发的开源消息中间件&#xff0c;支持 Java 消息服务、集群、Spring 框架等。属于消息队列组件(消息队列组件&#xff1a;分布式系统中的重要组件&#xff0c;主要解决应用耦合、异步消息…

数据库|TiDB-Server API的高效应用指南

一、API介绍 1.Status 显示TiDB 连接数、版本和git_hash 信息 tidb-server_ip:status_port/status { "connections": 0, "version": "5.7.25-TiDB-v6.1.1", "git_hash": "5263a0abda61f102122735049fd0dfadc7b7f822" } 2.St…

mysql-sql-练习题-4-标记

标记 连续登录2-7天用户建表排名找规律 最大连胜次数建表只输出连胜结果输出所有连续结果 连续登录2-7天用户 建表 create table continuous_login(user_id1 integer comment 用户id,date_login date comment 登陆日期 ) comment 用户登录表;insert into continuous_login val…