为什么有了并发安全的集合还需要读写锁?

大家好,我是三友,这篇文章想来跟大家来探讨一下,在Java中已经提供了并发安全的集合,为什么有的场景还需要使用读写锁,直接用并发安全的集合难道不行么?

在java中,并发安全的集合有很多,这里我就选用常见的CopyOnWriteArrayList为例,来说明一下读写锁的价值到底提现在哪。

CopyOnWriteArrayList核心源码分析

接下来我们分析一下CopyOnWriteArrayList核心的增删改查的方法

成员变量

java;gutter:true; //独占锁 final transient ReentrantLock lock = new ReentrantLock(); //底层用来存放元素的数组 private transient volatile Object[] array;</p> <pre><code> add方法:往集合中添加某个元素 ;gutter:true;
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();
}
}

add操作先通过lock加锁,保证同一时刻最多只有一个线程可以操作。加锁成功获取到成员变量的数据,然后拷贝成员变量数组的元素到新的数组,再基于新的数据来添加元素,最后将新拷贝的数组通过setArray来替换旧的成员变量的数组。

remove方法:移除集合中的某个元素

java;gutter:true; public E remove(int index) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; E oldValue = get(elements, index); int numMoved = len - index - 1; if (numMoved == 0) setArray(Arrays.copyOf(elements, len - 1)); else { Object[] newElements = new Object[len - 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index + 1, newElements, index, numMoved); setArray(newElements); } return oldValue; } finally { lock.unlock(); } }</p> <pre><code> remove操作也要先获取到锁。它先是取出对应数组下标的旧元素,然后新建了一个原数组长度减1的新数组,将除了被移除的元素之外,剩余的元素拷贝到新的数组,最后再通过setArray替换旧的成员变量的数组。 set方法:将集合中指定位置的元素替换成新的元素 ;gutter:true;
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);

if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}

set方法跟add,remove操作一样得先获取到锁才能继续执行。将原数组的原有元素拷贝到新的数组上,在新的数组完成数据的替换,最后也是通过setArray替换旧的成员变量的数组。

size方法:获取集合中元素的个数

java;gutter:true; public int size() { return getArray().length; }</p> <pre><code> size方法操作很简单,就是简单地返回一下当前数组的长度。 迭代器的构造 ;gutter:true;
public Iterator iterator() {
return new COWIterator(getArray(), 0);
}

构造COWIterator的时候传入当前数组的对象,然后基于当前数组来遍历,也不需要加锁。

讲完CopyOnWriteArrayList源码,我们可以看出CopyOnWriteArrayList的核心原理就是在对数组进行增删改的时候全部都是先加独占锁,然后对原有的数组进行拷贝,然后基于新复制的数组进行操作,最后将这个新的数组替换成员变量的数组;而对于读的操作来说,都是不加锁的,是基于当前成员变量的数组的这一时刻的快照来读的。其实CopyOnWriteArrayList是基于一种写时复制的思想,写的时候基于新拷贝的数组来操作,之后再赋值给成员变量,读的时候是原有的数组,这样读写其实就是不是同一个数组,这样就避免了读写冲突的情况,这其实也体现了一种读写分离的思想,读写操作的是不同的数组。

CopyOnWriteArrayList适用场景

接下来我们来思考一下,CopyOnWriteArrayList适合使用在什么样的场景中。通过上面源码的分析,我们可以看出,所有的写操作,包括增删改都需要加同一把独占锁,所以同时只允许一个线程对数组进行拷贝赋值的操作,多线程并发情况下所有的操作都是串行执行的,势必会导致并发能力降低,同时每次操作都涉及到了数组的拷贝,性能也不太好;而所有的读操作都不需要加锁,所以同一时间可以允许大量的线程同时读,并发性能高。所以综上我们可以得出一个结论,那就是CopyOnWriteArrayList适合读多写少的场景。

CopyOnWriteArrayList的局限性

说完CopyOnWriteArrayList,我们来想一想它有没有什么缺点。看起来CopyOnWriteArrayList除了写的并发性能差点,好像没有什么缺点了。的确,单从性能来看,确实是这种情况,但是,从数据一致性的角度来看,CopyOnWriteArrayList的数据一致性能力较弱,属于数据弱一致性。所谓的弱一致性,你可以这么理解,在某一个时刻,读到的数据并不是当前这一时刻最新的数据。

就拿CopyOnWriteArrayList举例来说,当有个线程A正在调用add方法来添加元素,此时已经完成了数组的拷贝,并且也将元素添加到数组中,但是还没有将新的数组赋值给成员变量,此时,另一个线程B来调用CopyOnWriteArrayList的size方法,来读取集合中元素的个数,那么此时读到的元素个数其实是不包括线程A要添加的元素,因为线程A并没有将新的数组赋值给成员变量,这就导致了线程B读到的数据不是最新的数据,也就是跟实际的数据不一致。

所以,从上面我们可以看出,CopyOnWriteArrayList对于数据一致性的保证,还是比较弱的。其实不光是CopyOnWriteArrayList,其实Java中的很多集合,队列的实现对于数据一致性的保证都比较弱。

如何来保证数据的强一致性

那么有什么好的办法可以保证数据的强一致性么?当然,保证并发安全,加锁就可以完成,但是加什么锁可以保证数据读写安全和数据一致性,其实最简单粗暴的方法就是对所有的读写都加上同一把独占锁,这样保证所有的读写操作都是串行执行,那么读的时候,其他线程一定不能写,那么读的一定是最新的数据。

如果真的这么去加独占锁,的确能够保证读写安全,但是性能却会很差,这也是为什么CopyOnWriteArrayList的读不加锁的原因,其实CopyOnWriteArrayList在设计的时候,就是降低数据一致性来换取读的性能。

那有没有什么折中的方法,既能保证读的性能不差,又能保证数据强一致性呢。这时就可以用读写锁来实现。所谓的读写锁,就是写的时候,其他线程不能写也不能读,读的时候,其他线程能读,但是不能写。也就是写写、读写互斥,但是读读不互斥。基于这种方式,就能保证读的时候,一定没有人在写,这样读到的数据就一定是最新的,同时也能保证其他线程也能读,不会出现上面举例的那种情况了,也就能保证数据的强一致性。读写锁相比独占锁而言,大大提高了读的并发能力,但是写的时候不能读,相比于CopyOnWriteArrayList而言,读的并发能力有所降低,这可能就是鱼(并发性能)和熊掌(数据一致性)不可兼得吧。

Java中也提供了读写锁的实现,ReentrantReadWriteLock,底层是基于AQS来实现的。有兴趣的小伙伴可以翻一下源码,看看是如何实现的,这里就不再剖析源码了。

总结

好了,通过这篇文章,想必大家知道为什么有并发安全的集合之后,还需要读写锁的原因,因为很多并发安全的集合对于数据一致性的保证是比较弱的,一旦遇到对于数据一致性要求比较高的场景,一些并发安全的集合就不适用了;同时为了避免独占锁带来的性能问题,可以选择读写锁来保证读的并发能力。小伙伴们在实际应用中需要根据应用场景来灵活地选择使用并发安全的集合、读写锁或者是独占锁,其实永远没有最好的选择,只有更好的选择。

以上就是本篇文章的全部内容,如果你有什么不懂或者想要交流的地方,欢迎关注我的个人的微信公众号 三友的java日记 ,我们下篇文章再见。

如果觉得这篇文章对你有所帮助,还请帮忙点赞、在看、转发一下,码字不易,非常感谢!

最近花了一个月的时间,整理了这套并发编程系列的知识点。涵盖了 volitile、synchronized、CAS、AQS、锁优化策略、同步组件、数据结构、线程池、Thread、ThreadLocal,几乎覆盖了所有的学习和面试场景,如图。

为什么有了并发安全的集合还需要读写锁?

为什么有了并发安全的集合还需要读写锁?

为什么有了并发安全的集合还需要读写锁?

为什么有了并发安全的集合还需要读写锁?

为什么有了并发安全的集合还需要读写锁?

文档获取方式:扫描二维码或者搜一搜关注微信公众号 三友的java日记 ,回复 并发 就能获取了。

为什么有了并发安全的集合还需要读写锁?

Original: https://www.cnblogs.com/zzyang/p/16319500.html
Author: 三友的java日记
Title: 为什么有了并发安全的集合还需要读写锁?

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/622621/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 软件工程 毕业设计题目汇总

    软件工程毕业设计 题目汇总 【不断更新中】 微信小程序 校园表白墙微信小程序 【地址:程序地址】 房屋租赁管理系统 【地址:程序地址】 航空售票管理系统 高校会议室管理系统 高校就…

    Java 2023年6月8日
    093
  • Java四类八种数据类型

    第一类:逻辑型boolean 第二类:文本型char 第三类:整数型(byte、short、int、long) char类型占2个字节short从-32768到32767int从-…

    Java 2023年5月29日
    083
  • Spring Cloud 还没学明白,Istio 又是什么鬼??

    背景 过去,我们运维着”能做一切”的大型单体应用程序。这是一种将产品推向市场的很好的方式,因为刚开始我们也只需要让我们的第一个应用上线。 而且我们总是可以回…

    Java 2023年5月29日
    088
  • Spring(一)-初识 + DI+scope

    1、获取bean实例的三种方式 UTF-8 4.3.18.RELEASE 1.16.18 4.11 org.springframework spring-beans ${sprin…

    Java 2023年6月15日
    082
  • Spring和Springboot相关知识点整理

    简介 本文主要整理一些Spring & SpringBoot应用时和相关原理的知识点,对于源码不做深入的讲解。 思维导图 右键新窗口打开可以放大。 说明 使用@Config…

    Java 2023年5月30日
    075
  • 03、SpringBoot 启动 执行SpringApplication的run方法 准备运行环境前 流程

    目录:Springboot源码学习目录上文:02、SpringBoot 启动 总流程前言:本篇文章开始讲;SpringBoot的启动流程的第二部分,当前第二部分是核心逻辑,也分为几…

    Java 2023年6月13日
    077
  • 单个表上亿行数据的主键、索引设计,及分页查询

    一,概述 一般而言,我们对关系型数据库系统,进行表结构设计时,会按数据的种类,进行分类,一般有如下种类: 1)主数据,其数据量基本稳定,不随时间而线性增长。比如,分公司,产品,经销…

    Java 2023年6月9日
    069
  • Java并发编程:线程池

    一、为什么使用线程池 使用线程的时候直接就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频…

    Java 2023年6月5日
    080
  • 用户管理

    用户管理 添加用户 基本语法 useradd 用户名 示例:添加一个用户名为tom的用户 useradd tom 细节说明 当创建用户成功后,会自动的创建和用户名同名的家目录 也可…

    Java 2023年6月5日
    092
  • Java 热更新 Groovy 实践及踩坑指南

    Apache的Groovy是Java平台上设计的面向对象编程语言。这门动态语言拥有类似Python、Ruby和Smalltalk中的一些特性,可以作为Java平台的脚本语言使用,G…

    Java 2023年6月15日
    061
  • Java基础随笔3

    我们目前在写程序的时候,数据值都是固定的,但是实际开发中,数据值肯定是变化的,所以,把数据改进为键盘录入,提高程序的灵活性。 键盘录入数据的步骤: A:导包(位置放到class定义…

    Java 2023年6月5日
    057
  • Nginx 负载均衡

    Nginx简单实现网站的负载均衡 地址:http://www.cnblogs.com/alvin_xp/p/4161162.html Original: https://www.c…

    Java 2023年5月30日
    057
  • 【前端】在浏览器控制台,直接发Ajax请求

    我们在日常的开发的过程中,经常需要前端测试发送请求测试一些数据。但是由于一些session,cookie的存在,我们无法在postman上创建一些会话。那么这样,我们就可以在浏览器…

    Java 2023年6月7日
    081
  • fast json 乱序问题解决过程

    解决问题:保存到redis中的jsonstring在转回jsonObject的时候乱序; 解决方案:https://inlhx.iteye.com/blog/2312512 解决过…

    Java 2023年6月13日
    081
  • 设计模式之解释器模式

    解释器模式属于行为型模式;指给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达…

    Java 2023年6月5日
    069
  • 服务器无外网且无yum源的软件安装方式

    无外网部署,无本地yum源部署方法 V_1.0 Author:王欢 Date:2021-06-23 1.查询目标服务器的操作系统版本 2.创建对应版本的最小虚拟机 3.在虚拟机上使…

    Java 2023年6月9日
    067
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球