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垃圾回收掉。这也是为什么上面迭代器可以读取到数据的原因之一。

相关文章
|
20天前
|
XML Java 编译器
Java注解的底层源码剖析与技术认识
Java注解(Annotation)是Java 5引入的一种新特性,它提供了一种在代码中添加元数据(Metadata)的方式。注解本身并不是代码的一部分,它们不会直接影响代码的执行,但可以在编译、类加载和运行时被读取和处理。注解为开发者提供了一种以非侵入性的方式为代码提供额外信息的手段,这些信息可以用于生成文档、编译时检查、运行时处理等。
59 7
|
1月前
|
数据采集 人工智能 Java
Java产科专科电子病历系统源码
产科专科电子病历系统,全结构化设计,实现产科专科电子病历与院内HIS、LIS、PACS信息系统、区域妇幼信息平台的三级互联互通,系统由门诊系统、住院系统、数据统计模块三部分组成,它管理了孕妇从怀孕开始到生产结束42天一系列医院保健服务信息。
32 4
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
77 2
|
13天前
|
存储 JavaScript 前端开发
基于 SpringBoot 和 Vue 开发校园点餐订餐外卖跑腿Java源码
一个非常实用的校园外卖系统,基于 SpringBoot 和 Vue 的开发。这一系统源于黑马的外卖案例项目 经过站长的进一步改进和优化,提供了更丰富的功能和更高的可用性。 这个项目的架构设计非常有趣。虽然它采用了SpringBoot和Vue的组合,但并不是一个完全分离的项目。 前端视图通过JS的方式引入了Vue和Element UI,既能利用Vue的快速开发优势,
76 13
|
26天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
54 12
|
21天前
|
JavaScript 安全 Java
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
|
1月前
|
人工智能 监控 数据可视化
Java智慧工地信息管理平台源码 智慧工地信息化解决方案SaaS源码 支持二次开发
智慧工地系统是依托物联网、互联网、AI、可视化建立的大数据管理平台,是一种全新的管理模式,能够实现劳务管理、安全施工、绿色施工的智能化和互联网化。围绕施工现场管理的人、机、料、法、环五大维度,以及施工过程管理的进度、质量、安全三大体系为基础应用,实现全面高效的工程管理需求,满足工地多角色、多视角的有效监管,实现工程建设管理的降本增效,为监管平台提供数据支撑。
43 3
|
22天前
|
人工智能 移动开发 安全
家政上门系统用户端、阿姨端源码,java家政管理平台源码
家政上门系统基于互联网技术,整合大数据分析、AI算法和现代通信技术,提供便捷高效的家政服务。涵盖保洁、月嫂、烹饪等多元化服务,支持多终端访问,具备智能匹配、在线支付、订单管理等功能,确保服务透明、安全,适用于家庭生活的各种需求场景,推动家政市场规范化发展。
|
1月前
|
运维 自然语言处理 供应链
Java云HIS医院管理系统源码 病案管理、医保业务、门诊、住院、电子病历编辑器
通过门诊的申请,或者直接住院登记,通过”护士工作站“分配患者,完成后,进入医生患者列表,医生对应开具”长期医嘱“和”临时医嘱“,并在电子病历中,记录病情。病人出院时,停止长期医嘱,开具出院医嘱。进入出院审核,审核医嘱与住院通过后,病人结清缴费,完成出院。
98 3
|
1月前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。