百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

面试必问之 CopyOnWriteArrayList,你了解多少?

toyiye 2024-09-19 04:50 2 浏览 0 评论

一、摘要

在介绍 CopyOnWriteArrayList 之前,我们一起先来看看如下方法执行结果,代码内容如下:

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("1");
    list.add("2");
    list.add("1");
    System.out.println("原始list元素:"+ list.toString());
    //通过对象移除等于内容为1的元素
    for (String item : list) {
        if("1".equals(item)) {
            list.remove(item);
        }
    }
    System.out.println("通过对象移除后的list元素:"+ list.toString());
}

执行结果内容如下:

原始list元素:[1, 2, 1]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.example.container.a.TestList.main(TestList.java:16)

很遗憾,结果并没有达到我们想要的预期效果,执行之后直接报错!抛ConcurrentModificationException异常!

为啥会抛这个异常呢?

我们一起来看看, foreach 写法实际上是对 List.iterator() 迭代器的一种简写,因此我们可以从分析 List.iterator() 迭代器进行入手,看看为啥会抛这个异常。

ArrayList 类中的 Iterator 迭代器实现,源码内容:

通过代码我们发现 ItrArrayList 中定义的一个私有内部类,每次调用 nextremove 方法时,都会调用 checkForComodification 方法,源码如下:

/**修改次数检查*/
final void checkForComodification() {
	//检查List中的修改次数是否与迭代器类中的修改次数相等
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

checkForComodification 方法,实际上是用来检查 List 中的修改次数 modCount 是否与迭代器类中的修改次数 expectedModCount 相等,如果不相等,就会抛出 ConcurrentModificationException 异常!

那么问题基本上已经清晰了,上面的运行结果之所以会抛出这个异常,就是因为 List 中的修改次数 modCount 与迭代器类中的修改次数 expectedModCount 不相同造成的!

阅读过集合源码的朋友,可能想起Vector这个类,它不是 JDK 中 ArrayList 线程安全的一个版本么?

好的,为了眼见为实,我们把 ArrayList 换成 Vector 来测试一下,代码如下:

public static void main(String[] args) {
    Vector<String> list = new Vector<String>();
    //模拟10个线程向list中添加内容,并且读取内容
    for (int i = 0; i < 5; i++) {
        final int j = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //添加内容
                list.add(j + "-j");

                //读取内容
                for (String str : list) {
                    System.out.println("内容:" + str);
                }
            }
        }).start();
    }
}

执行程序,运行结果如下:

还是一样的结果,抛异常了, Vector 虽然线程安全,只不过是加了 synchronized 关键字,但是迭代问题完全没有解决!

继续回到本文要介绍的 CopyOnWriteArrayList 类,我们把上面的例子,换成 CopyOnWriteArrayList 类来试试,源码内容如下:

public static void main(String[] args) {
    //将ArrayList换成CopyOnWriteArrayList
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add("1");
    list.add("2");
    list.add("1");
    System.out.println("原始list元素:"+ list.toString());

    //通过对象移除等于11的元素
    for (String item : list) {
        if("1".equals(item)) {
            list.remove(item);
        }
    }
    System.out.println("通过对象移除后的list元素:"+ list.toString());
}

执行结果如下:

原始list元素:[1, 2, 1]
通过对象移除后的list元素:[2]

呃呵,执行成功了,没有报错!是不是很神奇~~

当然,类似上面这样的例子有很多,比如写10个线程向 list 中添加元素读取内容,也会抛出上面那个异常,操作如下:

public static void main(String[] args) {
    final List<String> list = new ArrayList<>();
    //模拟10个线程向list中添加内容,并且读取内容
    for (int i = 0; i < 10; i++) {
        final int j = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //添加内容
                list.add(j + "-j");

                //读取内容
                for (String str : list) {
                    System.out.println("内容:" + str);
                }
            }
        }).start();
    }
}

类似的操作例子就非常多了,这里就不一一举例了。

CopyOnWriteArrayList 实际上是 ArrayList 一个线程安全的操作类!

从它的名字可以看出, CopyOnWrite 是在写入的时候,不修改原内容,而是将原来的内容复制一份到新的数组,然后向新数组写完数据之后,再移动内存指针,将目标指向最新的位置。

二、简介

从 JDK1.5 开始 Java 并发包里提供了两个使用 CopyOnWrite 机制实现的并发容器,分别是 CopyOnWriteArrayListCopyOnWriteArraySet

从名字上看, CopyOnWriteArrayList 主要针对动态数组,一个线程安全版本的 ArrayList !

CopyOnWriteArraySet 主要针对集, CopyOnWriteArraySet 可以理解为 HashSet 线程安全的操作类,我们都知道 HashSet 基于散列表 HashMap 实现, 但是 CopyOnWriteArraySet 并不是基于散列表实现,而是基于 CopyOnWriteArrayList 动态数组实现!

关于这一点,我们可以从它的源码中得出结论,部分源码内容:

从源码上可以看出, CopyOnWriteArraySet 默认初始化的时候,实例化了 CopyOnWriteArrayList 类, CopyOnWriteArraySet 的大部分方法,例如 addremove 等方法都基于 CopyOnWriteArraySet 实现!

两者最大的不同点是, CopyOnWriteArrayList 可以允许元素重复,而 CopyOnWriteArraySet 不允许有重复的元素!

好了,继续来 BB 本文要介绍的CopyOnWriteArrayList类~~

打开 CopyOnWriteArrayList 类的源码,内容如下:

可以看到 CopyOnWriteArrayList 的存储元素的数组 array 变量,使用了 volatile 关键字保证的多线程下数据可见行;同时,使用了 ReentrantLock 可重入锁对象,保证线程操作安全。

在初始化阶段, CopyOnWriteArrayList 默认给数组初始化了一个对象,当然,初始化方法还有很多,比如如下我们经常会用到的一个初始化方法,源码内容如下:

这个方法,表示如果我们传入的是一个 ArrayList 数组对象,会将对象内容复制一份到新的数组中,然后初始化进去,操作如下:

List<String> list = new ArrayList<>();
...
//CopyOnWriteArrayList将list内容复制出来,并创建一个新的数组
CopyOnWriteArrayList<String> copyList = new CopyOnWriteArrayList<>(list);

CopyOnWriteArrayList 是对原数组内容进行复制再写入,那么是不是也存在多线程下操作也会发生冲突呢?

下面我们再一起来看看它的方法实现!

三、常用方法

3.1、添加元素

add() 方法是 CopyOnWriteArrayList 的添加元素的入口!

CopyOnWriteArrayList 之所以能保证多线程下安全操作, add() 方法功不可没,源码如下:

操作步骤如下:

  • 1、获得对象锁;
  • 2、获取数组内容;
  • 3、将原数组内容复制到新数组;
  • 4、写入数据;
  • 5、将array数组变量地址指向新数组;
  • 6、释放对象锁;

在 Java 中,独占锁方面,有2种方式可以保证线程操作安全,一种是使用虚拟机提供的 synchronized 来保证并发安全,另一种是使用 JUC 包下的 ReentrantLock 可重入锁来保证线程操作安全。

CopyOnWriteArrayList 使用了 ReentrantLock 这种可重入锁,保证了线程操作安全,同时数组变量 array 使用 volatile 保证多线程下数据的可见行!

其他的,还有指定下标进行添加的方法,如 add(int index, E element) ,操作类似,先找到需要添加的位置,如果是中间位置,则以添加位置为分界点,分两次进行复制,最后写入数据!

3.2、移除元素

remove() 方法是 CopyOnWriteArrayList 的移除元素的入口!

源码如下:

操作类似添加方法,步骤如下:

array
index

当然,移除的方法还有基于对象的 remove(Object o) ,原理也是一样的,先找到元素的下标,然后执行移除操作。

3.3、查询元素

get() 方法是 CopyOnWriteArrayList 的查询元素的入口!

源码如下:

public E get(int index) {
    //获取数组内容,通过下标直接获取
    return get(getArray(), index);
}

查询因为不涉及到数据操作,所以无需使用锁进行处理!

3.4、遍历元素

上文中我们介绍到,基本都是在遍历元素的时候因为修改次数与迭代器中的修改次数不一致,导致检查的时候抛异常,我们一起来看看 CopyOnWriteArrayList 迭代器实现。

打开源码,可以得出 CopyOnWriteArrayList 返回的迭代器是 COWIterator ,源码如下:

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

打开 COWIterator 类,其实它是 CopyOnWriteArrayList 的一个静态内部类,源码如下:

可以看出,在使用迭代器的时候,遍历的元素都来自于上面的 getArray() 方法传入的对象数组,也就是传递进来的 array 数组!

由此可见,CopyOnWriteArrayList 在使用迭代器遍历的时候,操作的都是原数组,没有像上面那样进行修改次数判断,所以不会抛异常!

当然,从源码上也可以得出, 使用 CopyOnWriteArrayList 的迭代器进行遍历元素的时候,不能调用 remove() 方法移除元素,因为不支持此操作!

如果想要移除元素,只能使用CopyOnWriteArrayList提供的remove()方法,而不是迭代器的remove()方法,这个需要注意一下!

四、总结

CopyOnWriteArrayList 是一个典型的读写分离的动态数组操作类!

在写入数据的时候,将旧数组内容复制一份出来,然后向新的数组写入数据,最后将新的数组内存地址返回给数组变量;移除操作也类似,只是方式是移除元素而不是添加元素;而查询方法,因为不涉及线程操作,所以并没有加锁出来!

因为 CopyOnWriteArrayList 读取内容没有加锁,在写入数据的时候同时也可以进行读取数据操作,因此性能得到很大的提升,但是也有缺陷, 对于边读边写的情况,不一定能实时的读到最新的数据 ,比如如下操作:

public static void main(String[] args) throws InterruptedException {
    final CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add("a");
    list.add("b");
    for (int i = 0; i < 5; i++) {
        final int j =i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //写入数据
                list.add("i-" + j);
                //读取数据
                for (String str : list) {
                    System.out.println("线程-" + Thread.currentThread().getName() + ",读取内容:" + str);
                }
            }
        }).start();
    }
}

新建5个线程向 list 中添加元素,执行结果如下:

可以看到,5个线程的读取内容有差异!

因此 CopyOnWriteArrayList 很适合读多写少的应用场景!

原文地址:http://www.cnblogs.com/dxflqm/p/15802092.html

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码