大佬,能给我讲讲什么是读写锁和锁的升级吗?

ゞ 浴缸里的玫瑰 2023-02-18 09:47 157阅读 0赞

故事开端

在一个阳光灿烂的早晨,熊某突然从别人的口中听到读写锁锁的升级这个名词,脑海中第一反应就是——“What‘s this???”,锁还能分读写?锁还能升级?我的天,怎么感觉在升级打怪一样。

于是熊某带着满头的疑虑去请求公司的技术大佬,这位技术大佬很耐心的给我说:”熊兄弟,并发编程没你想得那么简单的,下面让我一一为你道来。”于是,担起小板凳,拿着扇子马上开讲!
在这里插入图片描述

何为“读写锁”?

读写锁是一个很多地方都使用的技术,基本上实现读写锁都要遵循三个原则:

  1. 允许多个线程同时读一个共享变量
  2. 只能有一个线程对共享变量执行写操作
  3. 当一个线程执行写操作的时候其它线程不能读共享变量

从以上三个原则可以得出,写锁和读锁是互斥的,写的时候不能读,写只能一个写,读可以多个读。

为什么要用读锁

这时候熊某略为不解,就问技术老大:“既然读锁能够运行多个线程访问共享变量,那直接不要锁就好了,为什么还要那么麻烦弄个读锁?“

技术老大耐心讲解:”熊兄弟这问题提的不错,为什么需要读锁?上面讲述的三个原则的第三点是答案问题的。’当一个线程执行写操作的时候其它线程不能读共享变量‘,就是说当有线程对共享变量进行更新的时候,其它线程是不能进行读操作的,只有当写操作完成后才能进行读,确保读到的数据是最新的。如果不加读锁,当数据更新的时候有其它线程还在读,就会出现读到的不是最新数据的情况“。

熊某:“哦!!!!”
在这里插入图片描述

读写锁的实现

“说了这么多,能给我看看是怎么实现的吗?”,熊某说到。
“没问题,上代码!”,技术大佬道

  1. public class ReadWriteTest{
  2. private Map<String, Object> map = new HashMap<>();
  3. private final ReadWriteLock lock = new ReentrantReadWriteLock();
  4. private final Lock readLock = lock.readLock();
  5. private final Lock writeLock = lock.writeLock();
  6. Object read(String key){
  7. readLock.lock();
  8. System.out.println("-------获得读锁");
  9. try {
  10. return map.get(key);
  11. }finally {
  12. System.out.println("-------释放读锁");
  13. readLock.unlock();
  14. }
  15. }
  16. void write(String key, Object value){
  17. writeLock.lock();
  18. System.out.println("-------获得写锁");
  19. try {
  20. map.put(key, value);
  21. }finally {
  22. System.out.println("-------释放写锁");
  23. writeLock.unlock();
  24. }
  25. }
  26. public static void main(String[] args) {
  27. ReadWriteTest readWriteTest = new ReadWriteTest();
  28. readWriteTest.write("test", "熊小哥好帅!");
  29. System.out.println(readWriteTest.read("test"));
  30. }
  31. }

输出结果是:
-———获得写锁
-———释放写锁
-———获得读锁
-———释放读锁
熊小哥好帅!

代码这么看的话看不出什么,既然是并发编程就要加多几个线程来测试啦,下面是改良版:

  1. public class ReadWriteTest {
  2. private Map<String, Object> map = new HashMap<>();
  3. private final ReadWriteLock lock = new ReentrantReadWriteLock();
  4. private final Lock readLock = lock.readLock();
  5. private final Lock writeLock = lock.writeLock();
  6. Object read(String key) {
  7. System.out.println(Thread.currentThread().getName() + "-------尝试获得读锁");
  8. while (!readLock.tryLock())
  9. System.out.println(Thread.currentThread().getName() + "-------获得读锁失败");
  10. if (readLock.tryLock()) {
  11. System.out.println(Thread.currentThread().getName() + "-------获得读锁成功");
  12. try {
  13. return map.get(key);
  14. } finally {
  15. System.out.println(Thread.currentThread().getName() + "-------释放读锁");
  16. readLock.unlock();
  17. }
  18. }
  19. return null;
  20. }
  21. void write(String key, Object value) {
  22. System.out.println(Thread.currentThread().getName() + "-------尝试获得写锁");
  23. if (writeLock.tryLock()) {
  24. System.out.println(Thread.currentThread().getName() + "-------获得写锁成功");
  25. try {
  26. map.put(key, value);
  27. } finally {
  28. System.out.println(Thread.currentThread().getName() + "-------释放写锁");
  29. writeLock.unlock();
  30. }
  31. } else {
  32. System.out.println(Thread.currentThread().getName() + "-------获得写锁失败");
  33. }
  34. }
  35. public static void main(String[] args) throws InterruptedException {
  36. ReadWriteTest readWriteTest = new ReadWriteTest();
  37. Thread thread1 = new Thread(
  38. () -> {
  39. readWriteTest.write("test", "熊小哥好帅");
  40. });
  41. Thread thread2 = new Thread(
  42. () -> {
  43. readWriteTest.read("test");
  44. });
  45. Thread thread3 = new Thread(
  46. () -> {
  47. readWriteTest.write("test", "熊小哥好帅");
  48. });
  49. Thread thread4 = new Thread(
  50. () -> {
  51. readWriteTest.read("test");
  52. });
  53. thread1.setName("线程A");
  54. thread2.setName("线程B");
  55. thread3.setName("线程C");
  56. thread4.setName("线程D");
  57. thread1.start();
  58. thread2.start();
  59. thread3.start();
  60. thread4.start();
  61. }
  62. }

这边为了得到加锁释放成功的结果,就用了tryLock(),运行结果为:
线程A———-尝试获得写锁
线程D———-尝试获得读锁
线程C———-尝试获得写锁
线程C———-获得写锁失败
线程B———-尝试获得读锁
。。。。。。。。省略了无数次的获得读锁失败
线程A———-释放写锁
线程D———-获得读锁失败
线程B———-获得读锁失败
线程D———-获得读锁成功
线程D———-释放读锁
线程B———-获得读锁成功
线程B———-释放读锁

线程AC负责获取写锁,线程BD负责获取读锁,看结果可以知道,当线程A获得写锁后,线程BD是不能获取读锁的,线程C也不能获取写锁。所以后面线程BD获取的数据是线程A修改后的数据。接下来我们看看多个线程能不能同时获取读锁,稍微改改测试代码:

  1. public static void main(String[] args) throws InterruptedException {
  2. ReadWriteTest readWriteTest = new ReadWriteTest();
  3. Thread thread2 = new Thread(
  4. () -> {
  5. readWriteTest.read("test");
  6. });
  7. Thread thread3 = new Thread(
  8. () -> {
  9. readWriteTest.read("test");
  10. });
  11. Thread thread4 = new Thread(
  12. () -> {
  13. readWriteTest.read("test");
  14. });
  15. thread2.setName("线程B");
  16. thread3.setName("线程C");
  17. thread4.setName("线程D");
  18. thread2.start();
  19. thread3.start();
  20. thread4.start();
  21. }

看看结果:
线程C———-尝试获得读锁
线程D———-尝试获得读锁
线程B———-尝试获得读锁
线程C———-获得读锁成功
线程C———-释放读锁
线程D———-获得读锁成功
线程B———-获得读锁成功
线程B———-释放读锁
线程D———-释放读锁

可以看到,多个线程可以同时获得读锁来访问变量。

值得注意的是,读写锁也是可重入锁
在这里插入图片描述

锁的升级

“读写锁的使用我基本明白,但是锁的升级呢?”
“熊兄弟莫要心急,本大佬马上给你解释。”
常说的锁升级,其实指的是从读锁升级为写锁,我们用上面的代码说下,这边改造下read()方法,为了方便,删除多余的代码:

  1. void read(String key) {
  2. // 这里上读锁
  3. if (readLock.tryLock()) {
  4. try {
  5. if(Objects.isNull(map.get(key))){
  6. try{
  7. // 这里上写锁
  8. writeLock.lock();
  9. map.put(key, "123");
  10. }finally {
  11. // 这里释放写锁
  12. writeLock.unlock();
  13. }
  14. }
  15. map.get(key);
  16. } finally {
  17. // 这里释放读锁
  18. readLock.unlock();
  19. }
  20. }
  21. }

执行一下,发现程序永久没执行完。这是咋回事?
正当熊某有这个问题时,技术大佬心领神会的说出了答案,“读写锁(ReadWriteLock)是不支持锁的升级,因为当读锁不释放的时候,获取写锁的线程只能永久等待,虽然不支持锁的升级,但是锁的降级还是支持的(降级指的是从写锁转换成读锁),你看看代码”:

  1. void read(String key) {
  2. // 这里上读锁
  3. if (readLock.tryLock()) {
  4. if(Objects.isNull(map.get(key))){
  5. // 发现为空释放读锁,然后上写锁
  6. readLock.unlock();
  7. writeLock.lock();
  8. // 再检测下是否有数据,避免重复存放数据,耗费资源
  9. try{
  10. if(Objects.isNull(map.get(key))){
  11. map.put(key, "熊小哥好帅");
  12. }
  13. // 这里降级为读锁
  14. readLock.lock();
  15. }finally {
  16. writeLock.unlock();
  17. }
  18. // 获取数据并释放读锁
  19. System.out.println(map.get(key));
  20. readLock.unlock();
  21. }
  22. }
  23. }

执行一下测试代码:

  1. public static void main(String[] args) throws InterruptedException {
  2. ReadWriteTest readWriteTest = new ReadWriteTest();
  3. Thread thread2 = new Thread(
  4. () -> {
  5. readWriteTest.read("test");
  6. });
  7. thread2.setName("线程B");
  8. thread2.start();
  9. }

看结果为:熊小哥好帅

结尾

“oh,我都懂了,我应该要怎么感谢技术大佬您呢?”熊某兴奋地叫道。
“今晚带我去吃海底捞!!”,技术大佬不客气的喊道。
“好吧好吧!”

最后说一句,各位喜欢的话可以点个赞哦。
在这里插入图片描述

发表评论

表情:
评论列表 (有 0 条评论,157人围观)

还没有评论,来说两句吧...

相关阅读

    相关 说说什么升级

    锁升级是指将原本设置的锁定机制进行升级,使其变得更加复杂和难以破解的过程。在计算机领域,锁是一种用于控制并发访问的技术,用于保护共享资源不被同时访问或修改而引发冲突或损坏。

    相关

    共享锁(S锁)又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S 锁。这保证了其他事务可以读A,但

    相关 重入

       在java多线程中,我们知道可以用synchronize关键字来实现线程间的同步互斥工作,还有更加优秀的机制去实现同步互斥工作,Lock对象。重入锁和读写锁,他们具有比

    相关 互斥

    一、线程互斥方式。 --- 互斥锁 1、什么是互斥锁?特点怎么样? 互斥锁是专门用于处理线程之间互斥的一种方式,它有两种:上锁状态/解锁状态。 如果互斥锁处于上锁状