jdk11源码--CopyOnWriteArrayList源码分析

简介: CopyOnWriteArrayList源码分析

@[toc]

概述

我们都知道CopyOnWriteArrayList是线程安全的列表,其内部是数组结构,并且适用于读多写少的应用场景。
当写比较频繁时不要使用CopyOnWriteArrayList,应该使用其他的数据结构代替。
接下来就从源码角度分析一下为什么会有以上的特性。

基本属性

//锁
final transient Object lock = new Object();

/** 内部真实存放数据的数据结构,它只能通过getArray/setArray方法访问. */
private transient volatile Object[] array;

其中lock是CopyOnWriteArrayList实现的关键。==类中所有的修改操作都会使用这个全局锁,保证只有一个线程可以对数据进行修改。==
array使用volatile 变量修饰,确保可见性,一个线程修改以后其他线程可以获取到最新修改后的值。

创建CopyOnWriteArrayList

创建一个空的CopyOnWriteArrayList时,比较简单,就是初始化一个长度是0的数组。

public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

add(E e)

添加元素的逻辑也是异常的简单粗暴:

  • 首先获取锁
  • 获取当前数组的长度
  • 现有数组copy到一个新的数组,新数组长度=现有数组长度+1
  • 将新元素添加到新数组最后一个位置
public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}

add(int index, E element)

想数组中指定位置添加元素。
原理同上,也是需要拷贝一份数组,并且长度+1.
唯一的区别是要拷贝两次,将index在新数组中的位置预留出来存放新的元素。详见下面代码注释

public void add(int index, E element) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException(outOfBounds(index, len));
        Object[] newElements;
        int numMoved = len - index;//计算需要移动几个元素。这里指的是index位置后面的元素需要逐个向后移动一位
        if (numMoved == 0)//新元素追加在末尾
            newElements = Arrays.copyOf(es, len + 1);
        else {
            newElements = new Object[len + 1];
            //通过两次System.arraycopy对数组进行复制,预留出index在新数组中对应的空位。
            System.arraycopy(es, 0, newElements, 0, index);
            System.arraycopy(es, index, newElements, index + 1,
                             numMoved);
        }
        newElements[index] = element;
        setArray(newElements);
    }
}

System.arraycopy

@HotSpotIntrinsicCandidate
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

参数:

  • Object src : 原数组
  • int srcPos : 从元数据的起始位置开始
  • Object dest : 目标数组
  • int destPos : 目标数组的开始起始位置
  • int length : 要copy的数组的长度

这是一个native方法,并且是有@HotSpotIntrinsicCandidate修饰的。所以该方法不仅是本地方法,而且是虚拟机固有方法。在虚拟机中通过手工编写汇编或其他优化方法来进行 Java 数组拷贝,这种方式比起直接在 Java 上进行 for 循环或 clone 是更加高效的。数组越大体现地越明显。

关于@HotSpotIntrinsicCandidate

这个注解是 HotSpot VM 标准的注解,被它标记的方法表明它为 HotSpot VM 的固有方法, HotSpot VM 会对其做一些增强处理以提高它的执行性能,比如可能手工编写汇编或手工编写编译器中间语言来替换该方法的实现。虽然这里被声明为 native 方法,但是它跟 JDK 中其他的本地方法实现地方不同,固有方法会在 JVM 内部实现,而其他的会在 JDK 库中实现。在调用方面,由于直接调用 JVM 内部实现,不走常规 JNI lookup,所以也省了开销。
由于这需要阅读JVM虚拟机中的源码,留作后续再深入研究。

get(int index)

get的方法和通常的数组操作一样。
注意:这里没有加锁,

public E get(int index) {
  return elementAt(getArray(), index);
}
final Object[] getArray() {
    return array;
}
static <E> E elementAt(Object[] a, int index) {
    return (E) a[index];
}

iterator 迭代器

CopyOnWriteArrayList中提供了迭代器的操作。

注意:==这个迭代器实际上是现有array的一个拷贝,所以他不用添加锁。但是会有脏数据的情况。因为在迭代期间,其他线程修改了数据以后,这个迭代器中拷贝的数据看不到最新的数据。==

Iterator()方法和listIterator()方法实现是一样的,只不过返回值的类型不同,在使用时差别不大:

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

public ListIterator<E> listIterator() {
    return new COWIterator<E>(getArray(), 0);
}

//这里面所有的修改方法均不可用
static final class COWIterator<E> implements ListIterator<E> {
    /** 数组array的快照 */
    private final Object[] snapshot;
    /** 游标  */
    private int cursor;

    COWIterator(Object[] es, int initialCursor) {
        cursor = initialCursor;
        snapshot = es;//在这里进行快照赋值
    }

    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    public boolean hasPrevious() {
        return cursor > 0;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    @SuppressWarnings("unchecked")
    public E previous() {
        if (! hasPrevious())
            throw new NoSuchElementException();
        return (E) snapshot[--cursor];
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor - 1;
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }

    public void set(E e) {
        throw new UnsupportedOperationException();
    }

    public void add(E e) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int size = snapshot.length;
        int i = cursor;
        cursor = size;
        for (; i < size; i++)
            action.accept(elementAt(snapshot, i));
    }
}

编写一个测试类来验证这个迭代器:

public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList();
        list.add(1);
        list.add(2);
        list.add(3);

        System.out.println(list.toString());

        new Thread(() -> {
            ListIterator<Integer> integerListIterator = list.listIterator();
            Integer next = integerListIterator.next();
            System.out.println(next+"*******");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            next = integerListIterator.next();
            System.out.println(next+"*******");
        }).start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.clear();
            System.out.println(list.toString());
        }).start();
    }

输出结果:

[1, 2, 3]
1*******
[]
2*******

可见,在其他线程修改了数据以后,迭代器里的还是老数据。这也就是上面所说的读到了脏数据。.

官方文档中有队这个iterater的注释:

 * <p>This is ordinarily too costly, but may be <em>more</em> efficient
 * than alternatives when traversal operations vastly outnumber
 * mutations, and is useful when you cannot or don't want to
 * synchronize traversals, yet need to preclude interference among
 * concurrent threads.  The "snapshot" style iterator method uses a
 * reference to the state of the array at the point that the iterator
 * was created. This array never changes during the lifetime of the
 * iterator, so interference is impossible and the iterator is
 * guaranteed not to throw {@code ConcurrentModificationException}.
 * The iterator will not reflect additions, removals, or changes to
 * the list since the iterator was created.  Element-changing
 * operations on iterators themselves ({@code remove}, {@code set}, and
 * {@code add}) are not supported. These methods throw
 * {@code UnsupportedOperationException}.

大体意思也就是说:迭代器创建的时候会生成一个对数组状态的引用(快照),也就是COWIterator.snapshot。这个快照在迭代器的声明周期中不会发生变化,不会收到其他线程的干扰。并且不会抛出ConcurrentModificationException异常。迭代器创建后,不支持修改的操作,也不会反映其他线程对CopyOnWriteArrayList数据的任何变更。

CopyOnWriteArrayList在每次更新时都会复制一份,然后修改。所以老的数据还在,除非被JVM垃圾回收掉。这也是为什么上面迭代器可以读取到数据的原因之一。

相关文章
|
1月前
|
Java Apache Maven
Java百项管理之新闻管理系统 熟悉java语法——大学生作业 有源码!!!可运行!!!
文章提供了使用Apache POI库在Java中创建和读取Excel文件的详细代码示例,包括写入数据到Excel和从Excel读取数据的方法。
56 6
Java百项管理之新闻管理系统 熟悉java语法——大学生作业 有源码!!!可运行!!!
|
2月前
|
数据采集 运维 前端开发
【Java】全套云HIS源码包含EMR、LIS (医院信息化建设)
系统技术特点:采用前后端分离架构,前端由Angular、JavaScript开发;后端使用Java语言开发。
70 5
|
4天前
|
运维 自然语言处理 供应链
Java云HIS医院管理系统源码 病案管理、医保业务、门诊、住院、电子病历编辑器
通过门诊的申请,或者直接住院登记,通过”护士工作站“分配患者,完成后,进入医生患者列表,医生对应开具”长期医嘱“和”临时医嘱“,并在电子病历中,记录病情。病人出院时,停止长期医嘱,开具出院医嘱。进入出院审核,审核医嘱与住院通过后,病人结清缴费,完成出院。
20 3
|
9天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
12天前
|
移动开发 前端开发 JavaScript
java家政系统成品源码的关键特点和技术应用
家政系统成品源码是已开发完成的家政服务管理软件,支持用户注册、登录、管理个人资料,家政人员信息管理,服务项目分类,订单与预约管理,支付集成,评价与反馈,地图定位等功能。适用于各种规模的家政服务公司,采用uniapp、SpringBoot、MySQL等技术栈,确保高效管理和优质用户体验。
|
1月前
|
JSON 前端开发 Java
震惊!图文并茂——Java后端如何响应不同格式的数据给前端(带源码)
文章介绍了Java后端如何使用Spring Boot框架响应不同格式的数据给前端,包括返回静态页面、数据、HTML代码片段、JSON对象、设置状态码和响应的Header。
119 1
震惊!图文并茂——Java后端如何响应不同格式的数据给前端(带源码)
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
365 37
|
25天前
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
19 1
|
1月前
|
存储 前端开发 Java
Java后端如何进行文件上传和下载 —— 本地版(文末配绝对能用的源码,超详细,超好用,一看就懂,博主在线解答) 文件如何预览和下载?(超简单教程)
本文详细介绍了在Java后端进行文件上传和下载的实现方法,包括文件上传保存到本地的完整流程、文件下载的代码实现,以及如何处理文件预览、下载大小限制和运行失败的问题,并提供了完整的代码示例。
280 1
|
2月前
|
传感器 监控 数据可视化
【Java】智慧工地解决方案源码和所需关键技术
智慧工地解决方案是一种新的工程全生命周期管理理念。它通过使用各种传感器、数传终端等物联网手段获取工程施工过程信息,并上传到云平台,以保障数据安全。
79 7