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

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

大家好,我是三友~~

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

公众号:三友的java日记

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

CopyOnWriteArrayList核心源码分析

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

成员变量

//独占锁
final transient ReentrantLock lock = new ReentrantLock();
//底层用来存放元素的数组
private transient volatile Object[] array;

add方法:往集合中添加某个元素

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方法:移除集合中的某个元素

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

remove操作也要先获取到锁。它先是取出对应数组下标的旧元素,然后新建了一个原数组长度减1的新数组,将除了被移除的元素之外,剩余的元素拷贝到新的数组,最后再通过setArray替换旧的成员变量的数组。

set方法:将集合中指定位置的元素替换成新的元素

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方法:获取集合中元素的个数

public int size() {
   
  return getArray().length;
}

size方法操作很简单,就是简单地返回一下当前数组的长度。

迭代器的构造

public Iterator<E> iterator() {
   
  return new COWIterator<E>(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来实现的。有兴趣的小伙伴可以翻一下源码,看看是如何实现的,这里就不再剖析源码了。

总结

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

PS:如果觉得这篇文章对你有帮助,欢迎大家关注公众号三友的java日记、分享、点赞、在看,感谢支持。

往期热门文章推荐

如何去阅读源码,我总结了18条心法

如何写出漂亮代码,我总结了45个小技巧

三万字盘点Spring/Boot的那些常用扩展点

三万字盘点Spring 9大核心基础功能

万字+20张图剖析Spring启动时12个核心步骤

1.5万字+30张图盘点索引常见的11个知识点

两万字盘点那些被玩烂了的设计模式

搜索关注公众号 三友的java日记 ,及时干货不错过,公众号致力于通过画图加上通俗易懂的语言讲解技术,让技术更加容易学习,回复 面试 即可获得一套面试真题。

相关文章
|
前端开发 JavaScript Java
Apache Wicket 框架:踏上从新手到英雄的逆袭之路,成就你的编程传奇!
【9月更文挑战第4天】Apache Wicket是一款基于Java的开源Web应用框架,以简洁、易维护及强大功能著称。它采用组件化设计,让页面开发更为模块化。Wicket的简洁编程模型、丰富的组件库、良好的可维护性以及对Ajax的支持,使其成为高效开发Web应用的理想选择。下文将通过解析Wicket的基本概念与特性,帮助读者深入了解这一框架的优势。
681 1
|
数据采集 监控 数据可视化
Fortran 在单位网络监控软件数据处理中的应用
在数字化办公环境中,Fortran 语言凭借其高效性和强大的数值计算能力,在单位网络监控软件的数据处理中展现出独特优势。本文介绍了 Fortran 在数据采集、预处理和分析可视化三个阶段的应用,展示了其在保障网络安全稳定运行和有效管理方面的价值。
174 10
|
算法 异构计算
基于FPGA的图像缩小算法实现,包括tb测试文件和MATLAB辅助验证
基于FPGA的图像缩小算法实现,包括tb测试文件和MATLAB辅助验证
|
前端开发
HTML和CSS实现品优购首页(三)
HTML和CSS实现品优购首页
|
存储 安全 Go
第十九章 CSP Session 管理 - %CSP.Session 对象
第十九章 CSP Session 管理 - %CSP.Session 对象
280 0
第十九章 CSP Session 管理 - %CSP.Session 对象
|
存储 数据可视化 关系型数据库
MySQL为什么用B+树做索引存储结构?
小白晋级大师第1篇文章,开始写一些有深度的文章了
254 0
MySQL为什么用B+树做索引存储结构?
|
网络协议 网络安全 UED
网络编程六-网络编程相关面试题汇总
网络编程六-网络编程相关面试题汇总
355 0
|
JSON 数据格式 Python
logstash6.8.12动态生成elasticsearch的index的正确方法
网上有很多的【假】logstash动态生成index的文章,看了很多,根本不符合我的需求,所以我决定来一篇干货,真正的解决问题。人狠话不多,代码直接上。我是使用官方提供的helm包进行ELK安装的
1437 0
logstash6.8.12动态生成elasticsearch的index的正确方法
|
前端开发 JavaScript API
记一次老项目中的跨页面通信问题和前端实现文件下载功能
由于笔者之前维护了几个比较老的项目是用jquery全家桶开发的,其中有些需求是需要跨页面交互和父子页面通信,故借此总结一下。另一块是前端实现文件下载功能,虽然方法很多,为了不用重复造轮子,在此还是总结一波,毕竟多页面下的应用场景还是很多的。
405 0

热门文章

最新文章