三种线程安全的List

news2024/11/24 17:48:43

在单线程开发环境中,我们经常使用ArrayList作容器来存储我们的数据,但它不是线程安全的,在多线程环境中使用它可能会出现意想不到的结果。

多线程中的ArrayList:

我们可以从一段代码了解并发环境下使用ArrayList的情况:

public class ConcurrentArrayList {
    public static void main(String[] args) throws InterruptedException {
        List<Integer> list = new ArrayList<>();

        Runnable runnable = () -> {
            for (int i = 0; i < 10000; i++) {
                list.add(i);
            }
        };
        
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        
        Thread.sleep(500);
        System.out.println(list.size());
    }
}
123456789101112131415161718

代码中循环创建了两个线程,这两个线程都执行10000次数组的添加操作,理论上最后输出的结果应该为20000,但经过多次尝试,最后只出现了两种结果:

  1. 数组索引越界异常
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 10
	at java.util.ArrayList.add(ArrayList.java:463)
	at ConcurrentArrayList.lambda$main$0(ConcurrentArrayList.java:14)
	at java.lang.Thread.run(Thread.java:748)
10007
12345
  1. 输出结果小于20000
16093
1

虽然仍有可能得到20000的结果,但概率非常低。我们要从ArrayList的源码中去分析为什么会出现这种结果。
ArrayList数组默认初始化大小:

// 默认初始大小
private static final int DEFAULT_CAPACITY = 10;
...
// 数组size
private int size;
12345

ArrayList的add方法:


public boolean add(E e) {
    //确定集合的大小是否足够,如果不够则会进行扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
1234567

以上面错误1:ArrayIndexOutOfBoundsException: 10为例,出现错误的步骤如下:

  1. 假设某时刻Thread-0和Thread-1都执行到了elementData[size++] = e; 这步,获取的size大小都为9,此时轮到Thread-1执行
  2. Thread-1执行elementData[9] = e,空间刚刚好够用,赋值完后size变为10。接着轮到Thread-0执行
  3. 因为Thread-0已经跳过了ensureCapacityInternal(size + 1); 这步判断容量的检查步骤,因此它执行elementData[10] = e,而数组容量刚好为10!此时就出现了数组越界的错误。

另外,size++本身就是非原子性的,多个线程之间访问冲突,这时两个线程可能对同一个位置赋值,这就出现了出现size小于期望值的错误2结果。

线程安全的List

目前比较常用的构建线程安全的List有三种方法:

  1. 使用Vector容器
  2. 使用Collections的静态方法synchronizedList(List< T> list)
  3. 采用CopyOnWriteArrayList容器

1.使用Vector容器

Vector类实现了可扩展的对象数组,并且它是线程安全的。它和ArrayList在常用方法的实现上很相似,不同的只是它采用了同步关键词synchronized修饰方法。
ArrayList中的add方法:

public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
123456789

Vector中的add方法:

public void add(int index, E element) {
    insertElementAt(element, index);
}
...
// 使用了synchronized关键词修饰
public synchronized void insertElementAt(E obj, int index) {
        modCount++;
        if (index > elementCount) {
            throw new ArrayIndexOutOfBoundsException(index
                                                     + " > " + elementCount);
        }
        ensureCapacityHelper(elementCount + 1);
        System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
        elementData[index] = obj;
        elementCount++;
    }
12345678910111213141516

可以看出,Vector在通用方法的实现上ArrayList并没有什么区别(这里不比较扩容方式等细节)

2. Collections.synchronizedList(List< T> list)

使用这种方法我们可以获得线程安全的List容器,它和Vector的区别在于它采用了同步代码块实现线程间的同步。通过分析源码,它的底层使用了新的容器包装原始的List。
下图是新容器的继承关系图:
在这里插入图片描述
synchronizedList方法:

public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }
12345

因为ArrayList实现了RandomAccess接口,因此该方法返回一个SynchronizedRandomAccessList实例。
该类的add实现:

public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}
123

其中,mutex是final修饰的一个对象:

final Object mutex;
1

我们可以看到,这种线程安全容器是通过同步代码块来实现的,基础的add方法任然是由ArrayList实现。

我们再来看看它的读方法:

public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}
123

和写方法没什么区别,同样是使用了同步代码块。线程同步的实现原理非常简单!

通过上面的分析可以看出,无论是读操作还是写操作,它都会进行加锁,当线程的并发级别非常高时就会浪费掉大量的资源,因此某些情况下它并不是一个好的选择。针对这个问题,我们引出第三种线程安全容器的实现。

3. CopyOnWriteArrayList

顾名思义,它的意思就是在写操作的时候复制数组。为了将读取的性能发挥到极致,在该类的使用过程中,读读操作和读写操作都不互斥,这是一个很神奇的操作,接下来我们看看它如何实现。

    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();
        }
    }
12345678910111213141516

从CopyOnWriteArrayList的add实现方式可以看出它是通过lock来实现线程间的同步的,这是一个标准的lock写法。那么它是怎么做到读写互斥的呢?

// 复制数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 赋值
newElements[len] = e;
1234

真实实现读写互斥的细节就在这两行代码上。在面临写操作的时候,CopyOnWriteArrayList会先复制原来的数组并且在新数组上进行修改,最后再将原数组覆盖。如果写操作的过程中发生了线程切换,并且切换到读线程,因为此时数组并未发生覆盖,读操作读取的还是原数组。

换句话说,就是读操作和写操作位于不同的数组上,因此它们不会发生安全问题。

另外,数组定义private transient volatile Object[] array,其中采用volatile修饰,保证内存可见性,读取线程可以马上知道这个修改。

private transient volatile Object[] array;
1

三种方式的性能比较

1. 首先我们来看看三种方式在写操作的情况:

public class ConcurrentList {
    public static void main(String[] args) {
        testVector();
        testSynchronizedList();
        testCopyOnWriteArrayList();
    }

    public static void testVector(){
        Vector vector = new Vector();
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            vector.add(i);
        }
        long time2 = System.currentTimeMillis();
        System.out.println("vector: "+(time2-time1));
    }

    public static void testSynchronizedList(){
        List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            list.add(i);
        }
        long time2 = System.currentTimeMillis();
        System.out.println("synchronizedList: "+(time2-time1));
    }

    public static void testCopyOnWriteArrayList(){
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            list.add(i);
        }
        long time2 = System.currentTimeMillis();
        System.out.println("copyOnWriteArrayList: "+(time2-time1));
    }
}
12345678910111213141516171819202122232425262728293031323334353637

在代码中我让Vector和SynchronizedList两种实现方式进行写操作10000000次,而CopyOnWriteArrayList仅仅只有100000次,与前两种方式少了100倍!
而结果却出乎意料:

vector: 3202
synchronizedList: 1795
copyOnWriteArrayList: 8159
123

第三种方式使用的时间远大于前两种,写操作越多,时间差就越明显。

看似出乎意料,实则意料之中,copyOnWriteArrayList每进行一次写操作都会复制一次数组,这是非常耗时的操作,因此在面临巨大的写操作量时才会差异这么大。

不过前两种方式之间为什么差异也很明显?可能因为同步代码块比同步方法效率更高?但是同步代码块是直接包含ArrayList的add方法,理论上两种同步方式应该差异不大,欢迎大佬指点。

我们再来看看三种方式在读操作的情况:

2. 我们再来看看三种方式在读操作的情况:

public class ConcurrentList {
    public static void main(String[] args) {
        testVector();
        testSynchronizedList();
        testCopyOnWriteArrayList();
    }

    public static void testVector(){
        Vector<Integer> vector = new Vector<>();
        vector.add(0);
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            vector.get(0);
        }
        long time2 = System.currentTimeMillis();
        System.out.println("vector: "+(time2-time1));
    }

    public static void testSynchronizedList(){
        List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
        list.add(0);
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            list.get(0);
        }
        long time2 = System.currentTimeMillis();
        System.out.println("synchronizedList: "+(time2-time1));
    }

    public static void testCopyOnWriteArrayList(){
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        list.add(0);
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            list.get(0);
        }
        long time2 = System.currentTimeMillis();
        System.out.println("copyOnWriteArrayList: "+(time2-time1));
    }
}
12345678910111213141516171819202122232425262728293031323334353637383940

这一次三种方式都进行了10000000次读操作,结果如下:

vector: 217
synchronizedList: 224
copyOnWriteArrayList: 12
123

这次copyOnWriteArrayList的优势就显示出来了,它的读操作没有实现同步,因此加快了多线程的读操作。其他两种方式的差别不大。

总结

  1. 获取线程安全的List我们可以通过Vector、Collections.synchronizedList()方法和CopyOnWriteArrayList三种方式
  2. 读多写少的情况下,推荐使用CopyOnWriteArrayList方式
  3. 读少写多的情况下,推荐使用Collections.synchronizedList()的方式

参考:

  1. 并发容器(二)—线程安全的List
  2. SynchronizedList和Vector的区别

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

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

相关文章

用友滴滴:商旅费控领域现实中的1+1>2

当前&#xff0c;AI、5G、大数据等数智技术持续演进&#xff0c;千行百业数智化正在加速。在行业数字化之下&#xff0c;企业服务生态也进入新发展阶段&#xff0c;同时也孕育着新合作、新机遇。 在2023用友生态峰会上&#xff0c;用友与滴滴签署战略合作。用友依托数智化商旅费…

塔望3W消费战略产品案丨筷手小厨:筷手功夫饭的差异化致胜之道

筷手小厨 自热米饭 客户&#xff1a;颐海国际控股有限公司 品牌&#xff1a;筷手小厨 服务&#xff1a;3W消费战略 自热米饭产品案 项目背景 颐海国际控股有限公司,成立于2013年。2016年在香港主板上市&#xff08;股票代码&#xff1a;01579.HK&#xff09;。是一家集研…

使用随机森林回归填补缺失值

文章目录 一、概述二、实现1. 导入需要的库2. 加载数据集3. 构造缺失值4. 使用0和均值填充缺失值5. 使用随机森林填充缺失值6. 对填充好的数据进行建模7. 评估效果对比 一、概述 现实中收集的数据&#xff0c;几乎不可能是完美无缺的&#xff0c;往往都会有一些缺失值。面对缺…

简单沟通,远离纸质繁琐!灵活简单的在线表单制作工具

在线表单制作表单的便利性、灵活性、简易性等优势特点已经成为当今现代化办公中的必需品。随着科技的进步和发展&#xff0c;大到企业、学校&#xff0c;小到集体、个人都需要采用正确的方式去采集信息。鉴于此&#xff0c;在线表单制作工具就是比较理想的记录工具了。那么&…

独立储能的现货电能量与调频辅助服务市场出清协调机制(Matlab代码实现)

&#x1f4a5; &#x1f4a5; &#x1f49e; &#x1f49e; 欢迎来到本博客 ❤️ ❤️ &#x1f4a5; &#x1f4a5; &#x1f3c6; 博主优势&#xff1a; &#x1f31e; &#x1f31e; &#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 …

中小型企业的仓库管理中常见问题及解决方案

中小型企业的仓储管理是企业运营中非常关键的一环&#xff0c;直接影响到企业的资金使用效率和客户体验。因此&#xff0c;建立高效的仓储管理系统对于企业来说至关重要。今天我们就来聊聊中小型企业该如何建立高效的仓库管理。 一、分析一下中小型企业仓库管理的现状 1、手工…

16.Java IO数据流

目录 1. Java基本介绍 2. JDK下载安装及其环境配置 3. 一个简单的java程序 4. Eclipse基本使用、数据类型、运算符 5. 控制语句&#xff08;if、switch、for、while、foreach&#xff09; 6. Java数组 7. Java字符串对象(String|StringBuffer|StringBuilder|StringJoiner…

功能安全ISO26262 道路车辆 功能安全审核及评估方法第3部分:软件层面

道路车辆 功能安全审核及评估方法 第3部分&#xff1a;软件层面 Road vehicles - Functional Safety Audit and Assessment Method - Part 3: Software Level &#xff08;工作组草案&#xff09; 2022.4.15 前  言 本文件按照GB/T 1.1—2020《标准化工作导则 第1部分…

什么是客户服务平台?

在社交媒体和智能手机出现之前&#xff0c;品牌主要通过单向广告渠道与客户互动。社交媒体打破了这种自上而下的动态&#xff0c;以前所未有的方式打开了对话&#xff0c;将客户包括在内。 品牌不再控制客户对人们分享公司内容的行为。人们可以点击离开&#xff0c;向左滑动&a…

分别使用软件和硬件SPI驱动1.28寸LCD屏幕刷新时间对比(驱动IC为GC9A01)

写在前面&#xff1a; 屏幕供应商提供的GC9A01驱动代码过于简陋&#xff0c;使用的话需要修改的东西比较多&#xff0c;还好看到一篇文章&#xff0c;而且博主分享了驱动代码。文章地址 以下软件SPI部分是通过博主分享的驱动代码得来的&#xff0c;稍微改一下就可以用&#xf…

jconsole工具监测jvm

背景 本篇文章为了记录如何使用jconsole工具。 jconsole工具使用环境是windows&#xff0c;监控服务端在linux上。 如何启动jconsole 找到你本地jdk的目录&#xff0c;进入bin目录&#xff0c;找到jconsole.exe程序双击启动即可 本地进程 如果是想调试本地程序的话&#xf…

23、iOS逆向防护

一、ptrace 1.1 、ptrace概述 ptrace: process trace,提供一个进程监察和控制另一个进程.并且可以读取和改变被控制进程的内存和寄存器里面的数据.它就可以用来实现断点调试和系统调用跟踪. App可以被lldb动态调试,因为App被设备中的debugserver附加,它会跟踪我们的应用进程…

Java阶段二Day08

Java阶段二Day08 文章目录 Java阶段二Day08DML语言-数据操作语言INSERTUPDATEDELETE 数据类型整数类型浮点类型字符类型日期类型 约束条件主键约束&#xff08;PRIMARY KEY&#xff09;非空约束&#xff08;NOT NULL&#xff09;唯一性约束&#xff08;UNIQUE&#xff09;检查约…

安科瑞:列头柜、监控系统、触摸屏的数据中心机房配电方案

摘要 安科瑞精密配电系统是安科瑞针对数据中心集中监控要求提供的多回路监控装置&#xff0c;监控多回路电参量并可对各种故障进行告警。主要适用于各类列头柜、精密配电柜、电源分配柜、UPS输出柜等末端配电设备的监控。在阐述数据中心机房机柜配电目标要求的基础上&#xff…

Redis分布式缓存方案

分布式缓存 单节点Redis问题 数据丢失&#xff1a;数据持久化并发能力弱&#xff1a;搭建主从集群&#xff0c;实现读写分离故障恢复问题&#xff1a;哨兵实现健康检测&#xff0c;自动恢复存储能力&#xff1a;搭建分片集群&#xff0c;利用插槽机制实现动态扩容 Redis持久…

Windows逆向安全(一)之基础知识(十八)

指针作为参数 代码 #include "stdafx.h" void function(int num){num52; } void function2(int* num){*num52; } int main(int argc, char* argv[]) {int num610;function(num);printf("%d\n",num);function2(&num);printf("%d\n",num);re…

KVM NAT 模型

目录 NAT原理 virbr0-nic作用 关于kvm中的网桥和virbr0-nic网卡需注意以下几点&#xff1a; NAT原理 virbr0-nic作用 在kvm中每次通过kvm创建一个网桥都会自动在该网桥下创建两个接口&#xff0c;与网桥同名的virbr0接口代表这个虚拟网桥&#xff0c;可以通过它来配置网桥…

vue2的生命周期

生命周期就是记录数据的状态。对数据进行操作 刚开始 new Vue() 创建了一个实例对象 beforeCreate() 数据还没有创建出来 created() 数据创建出来了&#xff0c;可以访问 判断有没有el 或 template 后 将模板编译成渲染函数 beforeMount() 数据还没有挂在到页面上面 mou…

引入tiff.js报错

当我们安装引入tiff.js时&#xff0c;会遇到这些问题&#xff0c;相关命令以及错误&#xff1a; 1、安装tiff依赖 npm install tiff.js 2、引入tiff import Tiff from tiff.js 3、错误展示&#xff1a; 这个错误是因为没有在vue.config.js以及webpack.base.conf.js(在build文…

C4D的GPU渲染器Octane和Redshift的渲染对比

对CG圈创作人员来说&#xff0c;除制作软件外渲染器是平时接触最多的一类软件&#xff0c;用渲染器进行渲染的过程&#xff0c;就是把制作软件里的预览效果变到融合材质、光照、物理特性的最终效果的这个过程&#xff0c;这是CG制作中最重要的一步&#xff0c;关乎着最终效果的…