大家好,欢迎来到IT知识分享网。
难度
初级
学习时间
30分钟
适合人群
零基础
开发语言
开发环境
- JDK v11
- IntelliJIDEA v2018.3
友情提示
- 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
- 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!
1.温故知新
前面在《“全栈2019”Java多线程第三十八章:从零手写一个线程安全缓冲区》一章中介绍了Lock与Condition实战项目:从零手写一个线程安全缓冲区。
在《“全栈2019”Java多线程第三十九章:显式锁实现生产者消费者模型》一章中介绍了用显式锁Lock与Condition对象来实现生产者与消费者模型。
在《“全栈2019”Java多线程第四十章:ReadWriteLock读写锁》一章中介绍了读写锁ReadWriteLock。
现在介绍读锁与写锁之间相互嵌套情况。
2.读锁与写锁之间相互嵌套例子
为了更加贴合实际需求,我们就来写一个缓存数据的例子,这个例子是简化版的缓存设计。
首先,创建一个缓存数据类:
然后,定义一个接收缓存数据的变量,暂且定义为String字符串类型(最后我们将其改为泛型):
接着,创建设置缓存数据的方法:
然后,创建获取缓存数据的方法:
经过上一章《“全栈2019”Java多线程第四十章:ReadWriteLock读写锁》的学习,知道了写入操作用写锁,读取操作用读锁。
接下来,我们将读写锁创建出来:
然后,创建出读锁和写锁:
接下来,在设置数据的方法上用写锁:
在获取数据的方法上用读锁:
缓存数据类先写到这,试一试效果如何。
小伙伴可能想问:说好的读锁与写锁嵌套呢?
别急,后面马上就来。
接下来,创建缓存数据类实例:
然后,创建设置缓存数据线程并启动。因为我们设置缓存数据只需要一个线程足够,所以Thread对象并重写run()方法即可:
接着,创建获取缓存数据的任务。因为我们要无限获取缓存数据,所以这里先创建实现了Runnable接口的匿名内部类对象,然后再创建Thread对象并将实现了Runnable接口的匿名内部类对象通过构造方法传递给Thread对象:
下面就开始无限创建获取缓存数据的线程:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
我们创建了一个设置缓存数据的线程和无限的获取缓存数据的线程,并且设置缓存数据的线程只设置了一次缓存数据,但有无数个获取缓存数据的线程在获取缓存数据。输出结果和预期的一致。
以上内容不是本章的重点,下面将引出读锁和写锁之间的嵌套情况。
缓存数据一般都是有过期时间的,比如说1分钟、1小时、1天或1周。
我们也来一个变量记录当前缓存数据创建时间:
为什么是记录缓存数据的创建时间?
当记录了缓存数据的创建时间之后,判断缓存数据是否过期只需用当前时间-缓存数据的创建时间得到时间差,再用时间差来跟过期时间比大小即可。若时间差大于过期时间,则缓存数据已过期,否则未过期。
举例:
过期时间为60秒
缓存数据创建时间为2019-03-16 12:00:00
当前时间为2019-03-16 12:00:30
时间差 = 缓存数据创建时间 – 当前时间 = 30秒(注意单位换算)
此时30<60,即时间差<过期时间,缓存数据未过期。
如上所述,修改缓存数据类,在类中定义一个用于记录缓存数据创建时间的变量:
该变量之所以是long类型的是因为待会我们会调用System.currentTimeMillis()方法来获取当前时间(单位:毫秒)。
接着,我们就在设置缓存数据的时候设置缓存数据创建的时间:
然后,进入本章主题。在获取缓存数据的方法里获取当前时间毫秒数:
接着,获取时间差,用缓存数据创建时间-当前时间:
然后,判断时间差是否大于过期时间,这里我们将过期时间设置为3秒钟,因为演示关系,不宜将时间设置太久,所以大家可在实际项目开发中根据需求而来:
接着,当缓存数据已过期时,将data设置为“数据已过期”:
此处有对data进行写入操作,理应用写锁将其同步起来:
此时读锁里面嵌套写锁,我们来运行程序看看效果如何。
运行程序,执行结果:
静图:
文字版:
hi,ReadWriteLock! hi,ReadWriteLock! hi,ReadWriteLock! [3.709s][warning][os,thread] Failed to start thread - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 4k, detached. Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached at java.base/java.lang.Thread.start0(Native Method) at java.base/java.lang.Thread.start(Thread.java:803) at main.Main.main(Main.java:38)
OutOfMemoryError是内存溢出错误,为什么会出现OutOfMemoryError呢?
那是因为我们读锁里面在嵌套写锁之前没有把读锁释放掉,导致持有写锁的线程和后面创建的众多获取缓存数据线程全等待了。大量线程被创建却没有被释放掉,所以内存消耗殆尽,产生OutOfMemoryError。
不明白没关系,我们把程序稍微改改,大家就明白了。
把无限创建获取缓存数据线程的代码,即以下代码:
换成如下代码,即使主线程睡4秒钟,然后创建一个获取缓存数据线程并执行:
例子改写完毕!
运行程序,执行结果:
从运行结果来看,符合预期。
程序一动也不动,要说让主线程睡4秒钟,这程序运行也不只4了,是什么原因导致程序在这不动的呢?
就是读锁里面嵌套写锁,还没有在获取写锁之前释放读锁导致的。
用图片介绍就是,一开始获取缓存数据的线程拿到读锁:
紧接着,又去拿了写锁:
当任何线程拿到写锁时,所有拿到读锁的线程将会被等待:
此时,线程还没死,只是被等待了,而且该线程还是前台线程,所以程序就被等待了。
不清楚什么是前台线程和后台线程的小伙伴可以前去查阅《“全栈2019”Java多线程第十二章:后台线程setDaemon()方法详解》一章。
当然了,上面是单个线程,换作是无限个线程也是类似的,线程被创建出来都被等待了,所以内存被消耗殆尽,最终产生OutOfMemoryError。
问题的解决办法是:在获取写锁之前释放掉读锁。
下面,我们来修改缓存数据类中的getCachedData()方法,在获取写锁之前释放读锁:
程序修改完毕。
运行程序,执行结果:
静图:
文字版:
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException: attempt to unlock read lock, not locked by current thread at java.base/java.util.concurrent.locks.ReentrantReadWriteLock$Sync.unmatchedUnlockException(ReentrantReadWriteLock.java:448) at java.base/java.util.concurrent.locks.ReentrantReadWriteLock$Sync.tryReleaseShared(ReentrantReadWriteLock.java:432) at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.releaseShared(AbstractQueuedSynchronizer.java:1382) at java.base/java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock.unlock(ReentrantReadWriteLock.java:897) at lab.CachedData.getCachedData(CachedData.java:93) at main.Main$2.run(Main.java:30) at java.base/java.lang.Thread.run(Thread.java:834)
发生IllegalMonitorStateException异常,一般是当前线程没有锁的情况还去操作跟锁相关的动作,例如,本例中的释放读锁操作:
是不是把此处的try-finally和释放读锁的代码移除掉就完了呢?
此处的代码不应该移除,而是应该让获取缓存数据的线程在释放写锁之前再次获取到读锁,因为“return data”也是一种读取操作。
如上所述,我们继续来修改getCachedData()方法,在释放写锁之后立即获取读锁:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
整个程序现在没有任何问题。
下面,我们把无限创建获取缓存数据线程的代码改回来试试:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
整个读锁与写锁嵌套的例子完整无误。
最后,希望大家可以把这个例子照着写一遍,然后再自己默写一遍,方便以后碰到类似的面试题可以轻松应对。
祝大家编码愉快!
GitHub
本章程序GitHub地址:https://github.com/gorhaf/Java2019/tree/master/Thread/ReadWriteLock
总结
- 写锁在某一时刻最多只能被一个线程拥有;而读锁在某一时刻可以被多个线程拥有。
- 所有ReadWriteLock实现(如ReentrantReadWriteLock实现类)都必须保证写锁操作的内存同步效果对相关的readLock也有效。也就是说,成功获取读锁的线程将看到在先前释放写锁时所做的所有更新。
- 读写锁适合用在多读少写的场景。
- 读锁与写锁之间相互嵌套需要注意获取/释放锁的顺序。
至此,Java中读锁与写锁之间相互嵌套情况相关内容讲解先告一段落,更多内容请持续关注。
答疑
如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。
上一章
“全栈2019”Java多线程第四十章:ReadWriteLock读写锁
下一章
“全栈2019”Java多线程第四十二章:获取线程与读写锁的保持数
学习小组
加入同步学习小组,共同交流与进步。
- 方式一:关注头条号Gorhaf,私信“Java学习小组”。
- 方式二:关注公众号Gorhaf,回复“Java学习小组”。
全栈工程师学习计划
关注我们,加入“全栈工程师学习计划”。
版权声明
原创不易,未经允许不得转载!
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/58647.html