[Java并发]线程安全的List

news/2024/9/19 13:10:20/文章来源:https://www.cnblogs.com/DCFV/p/18407189

线程安全的List

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

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

使用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++;}

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++;}

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

Collections.synchronizedList(List< T> list)

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

synchronizedList方法:

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

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

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

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

final Object mutex;

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

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

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

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

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

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

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

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

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

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

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

private transient volatile Object[] array;

三种方式的性能比较

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

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

vector: 3202
synchronizedList: 1795
copyOnWriteArrayList: 8159

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

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

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

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

  1. 我们再来看看三种方式在读操作的情况:
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));}
}

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

vector: 217
synchronizedList: 224
copyOnWriteArrayList: 12

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

总结

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

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

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

相关文章

章10——面向对象编程(高级部分)——内部类

重点掌握匿名内部类的使用! 1、内部类可以随意访问外部类的成员,包括私有的属性,而外部类不可以。 2、内外部类有重名属性时,内部类的访问采用就近原则,如想访问外部的成员,则用外部类名.this.属性名。内部类分类,四种局部内部类第七条解释:Outer02.this本质是一个外部…

【整理】虚拟地址全解析:操作系统内存管理与进程调度的深度揭秘!

原创 freedom47概述 在现代计算机系统中,虚拟地址是内存管理的关键组成部分。 虚拟地址不仅帮助操作系统高效地管理物理内存,还在进程的内存分配中发挥重要作用。 本文将详细介绍虚拟地址的定义、作用、操作系统的内存管理、进程内存分配、32 位与 64 位架构的内存分配差异,…

2024.9.10 搜索引擎+字体

今天是人工智能的第一节课!我们主要学了引擎的搜索以及字体两部分,干货满满!有一种走了20年弯路的感觉(⊙︿⊙)第一次拥有了博客账号,在我小学的时候我妈妈会用博客记录生活,对于博客有一种熟悉的陌生感hhha 【知识小课堂1】 搜索引擎分为两类: 一、目录式分类搜索引擎,…

The Teachers Day gift a future teacher wants

`#include include void printBanner(); void printHeart(); void printFlower(); int main() { std::cout << "\n"; printBanner(); std::cout << std::endl; printFlower(); std::cout << std::endl; printHeart();return 0;}`点击查看代码 vo…

解决路由缓存问题

路由缓存问题即:当再vue3中使用带参数的路由时,随着路由跳转,组件被重新复用,不能正常执行生命周期 尤其我们通常在onMounted中使用的请求的发送,那么如何解决呢 1.粗暴的方法:强制替换销毁 vue官方曾说,key可以强制替换一个元素或者组件,而不是复用它 那么我们可以在组件中使用…

第二章python基本语法

位运算符 例1:检测列表里重复元素l=[1,3,5,7,8,3,9,4,2,5,6]flag=0for i in range(len(l)): if(1<<l[i]&flag)>0: print("重复:%d"%l[i]) flag|=(1<<l[i])#flag=flag|(1<<l[i]) 注:flag记录已经出现过的元素,其实用对应位为…

VS安装插件,按CTRL+鼠标左键进入函数

1。菜单栏-》工具-》扩展和更新 2.进入扩展和更新,点击联机,vs库,右上输入“Go To Definition” 3.搜索 出插件,选择作者是“Noah Richards” 4.下载安装重启即可hello,world~~~

【Azure Service Bus】创建 ServiceBus 的Terraform脚本报错GetAuthorizationRule: Invalid input

问题描述 在使用Terraform部署Service Bus时候,遇见了如下报错: Error: Error making Read request on Azure ServiceBus Topic Authorization Rule : servicebus.TopicsClient#GetAuthorizationRule: Invalid input: autorest/validation: validation failed: parameter=aut…

IIC工作模式时序分析

IIC工作模式时序分析 此处利用IO口模拟IIC通信过程中的时序。通信过程 在IIC通信过程SDA存在两种模式(接收模式和发送模式),发送或接受一个字节(器件的7个bit+1个bit方向(1 - 读方向,0 - 写方向)) 模式配置当SDA为接入模式接收了1字节数据后在第九个时钟脉冲期间就要变…

第四章 视图(views)

4.视图 4.1 文件or文件夹4.2 相对和绝对导入urls注意实现:不要再项目根目录做相对导入。 原则:绝对导入 相对导入(层级深)4.3 视图参数 urlpatterns = [path(login/, account.login, name="login"),path(auth/, order.auth, name=auth), ]from django.shortcuts …