“全栈2019”Java多线程第四十八章:读写锁实战高并发容器

难度

初级

学习时间

30分钟

适合人群

零基础

开发语言

Java

开发环境

  • JDK v11
  • IntelliJIDEA v2018.3

友情提示

  • 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
  • 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!

1.温故知新

前面在《“全栈2019”Java多线程第四十三章:查询是否有线程在等待读写锁》一章中介绍了可重入读写锁ReentrantReadWriteLock的查询是否有线程正在等待获取读写锁hasQueuedThreads()方法、获取有多少个正在等待获取读写锁的线程getQueueLength()方法和查询指定线程是否正在等待获取读写锁的hasQueuedThread(Thread thread)方法。

《“全栈2019”Java多线程第四十四章:读锁不支持Condition》一章中介绍了可重入读写锁ReentrantReadWriteLock与Condition的关系,在读写锁中,写锁是支持Condition的,读锁是不支持Condition的,原因在于写锁在某一时刻最多只能被一个线程拥有,而读锁在某一时刻最多可以被多个线程拥有,对于读锁而言,其他线程没有必要等待获取读锁,等待唤醒是毫无意义的。

《“全栈2019”Java多线程第四十五章:查询Condition上的等待线程》一章中介绍了如何查询Condition上是否有线程正在等待和有多少线程正在等待。用锁的hasWaiters​(Condition condition)方法查询Condition上是否有线程正在等待,再用getWaitQueueLength​(Condition condition)方法获取Condition上等待的线程的个数。

《“全栈2019”Java多线程第四十六章:判断任意线程是否已持有写锁》一章中介绍了如何判断任意线程是否已持有写锁。通过ReentrantReadWriteLock的isWriteLocked()方法来判断写锁是否已被任意线程持有。

《“全栈2019”Java多线程第四十七章:判断锁是否为公平锁isFair()》一章中介绍了如何判断锁是否为公平锁isFair()方法。

现在介绍如何使用读写锁实战简易版高并发容器。

2.读写锁实战高并发容器

此次,我们来书写一个简易版高并发容器,代码很简单,就是结合之前学习的多线程知识加上集合。

为我们高并发容器类取一个名字,就叫ConcurrentDictionary。其中,Concurrent是并发的意思;Dictionary是字典的意思,Dictionary和Map是一类数据结构。

为什么不取名叫ConcurrentMap呢?

因为ConcurrentMap已经是Java官方高并发容器标准了,所以这里就不再取名ConcurrentMap来混淆视听了。

接下来,我们来创建ConcurrentDictionary类:

刚刚说过,Dictionary和Map数据结构一样,只是名字不同而已,所以在ConcurrentDictionary类中需要一个Map来作为我们的存储数据的容器:

Map可以添加泛型,这里我们暂时使用<String, String>,后面再修改为由用户指定的泛型:

容器已经有了。

接下来就是书写添加数据获取数据的功能。

先来书写添加数据的功能:

这里有一个问题:m.put()方法有返回值,这个返回值要不要返回出去?

答案是需要将m.put()方法有返回值返回出去,因为在内部我们无法自行处理

m.put()方法有返回值是什么?

覆盖之前的值

什么叫覆盖之前的值

还是通过一个动画来了解一下吧:

新值覆盖旧值,将旧值返回给调用者,这就是put()方法做的事情。

相应的,我们ConcurrentDictionary类里面的put()方法也需要将值返回给调用者

添加数据的功能暂时写完了。

接着,来书写获取数据的功能:

添加数据和获取数据的功能都书写完了,虽然说这个容器可以使用了,但是容器本身还不是线程安全的,需要加上锁。

接下来,我们就来创建出读写锁:

然后,创建出读锁和写锁:

接着,在存入数据的时候加上写锁

然后,在获取数据的时候加上读锁

ConcurrentDictionary类暂时写在这,先来试一试。

书写Main类,创建ConcurrentDictionary容器:

然后,创建一个存数据的任务:

接着,在run()方法中不停地调用ConcurrentDictionary实例的put()方法存数据

然后,在数据后面加上一个随机数,目的为了存入不同数据:

接着,在存完数据之后使当前线程睡1秒钟,为了待会获取数据的时候输出好看:

然后,创建获取数据的任务:

接着,在run()方法中不停地调用ConcurrentDictionary实例的get()方法获取数据并输出

然后,创建两个存数据的线程和两个获取数据的线程:

接着,启动线程:

例子书写完毕。

运行程序,执行结果:

从运行结果来看,符合预期。每次存储的数据都及时被更新,每次获取的数据都是最新更新的数据。

其实,一个简易版的并发容器到此就写完了,很简单,后面我也会和大家一起去读读Java官方的高并发容器是怎么写的,那个要比这个复杂一点。

最后呢,再添加两个方法,这两个方法是为了演示何时使用写锁和读锁的应用。

一个是获取容器中所有的key,还有一个是清空容器

3.获取容器中所有的key

首先,我们来看看获取容器中所有的key

方法返回List集合,泛型为String。

在Map接口中,有一个keySet()的方法可以返回Map集合中的所有键:

只不过keySet()方法返回的是Set集合不是List集合。所以,还需要进行转换,这里只需创建出ArrayList集合对象并将keySet()方法返回值传入即可:

因为获取容器中所有键的操作是一个读取操作,所以这里需要加上读锁:

加锁的目的就是为了线程安全,这里加上读锁还有一点就是及时反馈容器中数据的变化,以免造成误读。比如,我们把容器清空了,这时读取到的key集合应该是空的,如果没有加上读锁,就有可能出现我们获取到的key集合还是未清空容器之前的key集合,所以加上读锁是有必要的。

获取容器中的所有key集合功能写完了,接下来来用用。

还是之前的例子,只不过把main()方法中的内容移除一部分,只保留以下代码:

接着,我们存入三个不同的key-value:

然后,调用容器的allKeys方法,获取容器中所有key集合:

接着,变量key集合:

例子改写完毕。

运行程序,执行结果:

从运行结果来看,符合预期。确实是我们之前存入的三个key。

4.清空容器

接下来,我们就来写一个清空容器的方法:

在clear内部调用m.clear()方法即可:

因为清空容器是一个写入操作,所以需要加上写锁

至此,清空容器功能书写完毕。

接下来,来试一试效果。

还是上一小节的例子,在添加数据操作之后把容器清空:

例子改写完毕。

运行程序,执行结果:

从运行结果来看,符合预期。key集合为空。

今天简易版的高并发容器就写到这,未来我们一起从零书写一个基础版的高并发容器。

最后,希望大家可以把这个例子照着写一遍,然后再自己默写一遍,方便以后碰到类似的面试题可以轻松应对。

祝大家编码愉快!

GitHub

本章程序GitHub地址:https://github.com/gorhaf/Java2019/tree/master/Thread/ReadWriteLock/ReentrantReadWriteLock/ConcurrentDictionary

总结

  • 写入操作加上写锁。
  • 读取操作加上读锁。

至此,Java中读写锁实战高并发容器相关内容讲解先告一段落,更多内容请持续关注。

答疑

如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。

上一章

“全栈2019”Java多线程第四十七章:判断锁是否为公平锁isFair()

下一章

“全栈2019”Java多线程第四十九章:LockSupport简单介绍

学习小组

加入同步学习小组,共同交流与进步。

  • 方式一:关注头条号Gorhaf,私信“Java学习小组”。
  • 方式二:关注公众号Gorhaf,回复“Java学习小组”。

全栈工程师学习计划

关注我们,加入“全栈工程师学习计划”。

版权声明

原创不易,未经允许不得转载!

了解更多
举报
评论 0