多线程系列(十) -ReadWriteLock用法详解

打印 上一主题 下一主题

主题 908|帖子 908|积分 2724

一、摘要

在上篇文章中,我们讲到ReentrantLock可以保证了只有一个线程能执行加锁的代码。
但是有些时候,这种保护显的有点过头,比如下面这个方法,它仅仅就是只读取数据,不修改数据,它实际上允许多个线程同时调用的。
  1. public class Counter {
  2.     private final Lock lock = new ReentrantLock();
  3.     private int count;
  4.     public int get() {
  5.         // 加锁
  6.         lock.lock();
  7.         try {
  8.             return count;
  9.         } finally {
  10.             // 释放锁
  11.             lock.unlock();
  12.         }
  13.     }
  14. }
复制代码
站在程序性能的角度,实际上我们想要的是这样的效果。

  • 1.读和读之间不互斥,因为只读操作不会有数据安全问题
  • 2.写和写之间互斥,避免一个写操作影响另外一个写操作,引发数据计算错误问题
  • 3.读和写之间互斥,避免读操作的时候写操作修改了内容,引发数据脏读问题
总结起来就是,允许多个线程同时读,但只要有一个线程在写,其他线程就必须排队等待。
在 JDK 中有一个读写锁ReadWriteLock,使用它就可以解决这个问题,它可以保证以下两点:

  • 1.只允许一个线程写入,其他线程既不能写入也不能读取
  • 2.没有写入时,多个线程允许同时读,可以提高程序并发性能
实际上,读写锁ReadWriteLock里面有两个锁实现,一个是读操作相关的锁,称为共享锁,当多个线程同时操作时,不会让多个线程进行排队等待,大大的提升了程序并发读的执行效率;另一个是写操作相关的锁,称为排他锁,当多个线程同时操作时,只允许一个线程写入,其他线程进入排队等待;两者进行组合操作,就可以实现上面的预期效果。
下面我们一起来看看它的基本用法!
二、ReadWriteLock 基本用法

2.1、读和读共享

读和读之间不互斥,当多个线程进行读的时候,不会让多个线程进行排队等待。
我们可以看一个简单的例子!
  1. public class Counter {
  2.     private final ReadWriteLock lock = new ReentrantReadWriteLock();
  3.     private int count;
  4.     public void read() {
  5.         // 加读锁
  6.         lock.readLock().lock();
  7.         try {
  8.             String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
  9.             System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + "获得了读锁,count:" + count);
  10.             Thread.sleep(5000);
  11.         } catch (InterruptedException e) {
  12.             e.printStackTrace();
  13.         } finally {
  14.             // 释放读锁
  15.             lock.readLock().unlock();
  16.         }
  17.     }
  18. }
复制代码
  1. public class MyThreadTest {
  2.     public static void main(String[] args) {
  3.         Counter counter = new Counter();
  4.         Thread threadA = new Thread(new Runnable() {
  5.             @Override
  6.             public void run() {
  7.                 counter.read();
  8.             }
  9.         });
  10.         Thread threadB = new Thread(new Runnable() {
  11.             @Override
  12.             public void run() {
  13.                 counter.read();
  14.             }
  15.         });
  16.         threadA.start();
  17.         threadB.start();
  18.     }
  19. }
复制代码
看一下运行结果:
  1. 2023-10-23 16:12:28:119 当前线程:Thread-0获得了读锁,count:0
  2. 2023-10-23 16:12:28:119 当前线程:Thread-1获得了读锁,count:0
复制代码
从日志时间上可以很清晰的看到,尽管加锁了,并且休眠了 5 秒,但是两个线程还是几乎同时执行try()方法里面的代码,证明了读和读之间是不互斥的,可以显著提高程序的运行效率。
2.2、写和写之间互斥

写和写之间互斥,当多个线程进行写的时候,只允许一个线程写入,其他线程进入排队等待。
我们可以看一个简单的例子!
  1. public class Counter {
  2.     private final ReadWriteLock lock = new ReentrantReadWriteLock();
  3.     private int count;
  4.     public void write() {
  5.         // 加写锁
  6.         lock.writeLock().lock();
  7.         try {
  8.             count++;
  9.             String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
  10.             System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + "获得了写锁,count:" + count);
  11.             Thread.sleep(5000);
  12.         } catch (InterruptedException e) {
  13.             e.printStackTrace();
  14.         } finally {
  15.             // 释放写锁
  16.             lock.writeLock().unlock();
  17.         }
  18.     }
  19. }
复制代码
  1. public class MyThreadTest {
  2.     public static void main(String[] args) {
  3.         Counter counter = new Counter();
  4.         Thread threadA = new Thread(new Runnable() {
  5.             @Override
  6.             public void run() {
  7.                 counter.write();
  8.             }
  9.         });
  10.         Thread threadB = new Thread(new Runnable() {
  11.             @Override
  12.             public void run() {
  13.                 counter.write();
  14.             }
  15.         });
  16.         threadA.start();
  17.         threadB.start();
  18.     }
  19. }
复制代码
看一下运行结果:
  1. 2023-10-23 16:29:59:103 当前线程:Thread-0获得了写锁,count:1
  2. 2023-10-23 16:30:04:108 当前线程:Thread-1获得了写锁,count:2
复制代码
从日志时间上可以很清晰的看到,两个线程进行串行执行,证明了写和写之间是互斥的。
2.3、读和写之间互斥

读和写之间互斥,当多个线程交替进行读写的时候,操作上互斥,只有一个线程能进入,其他线程进入排队等待。
我们可以看一个简单的例子!
  1. public class Counter {
  2.     private final ReadWriteLock lock = new ReentrantReadWriteLock();
  3.     private int count;
  4.     public void read() {
  5.         // 加读锁
  6.         lock.readLock().lock();
  7.         try {
  8.             String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
  9.             System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + "获得了读锁,count:" + count);
  10.             Thread.sleep(5000);
  11.         } catch (InterruptedException e) {
  12.             e.printStackTrace();
  13.         } finally {
  14.             // 释放读锁
  15.             lock.readLock().unlock();
  16.         }
  17.     }
  18.     public void write() {
  19.         // 加写锁
  20.         lock.writeLock().lock();
  21.         try {
  22.             count++;
  23.             String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
  24.             System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + "获得了写锁,count:" + count);
  25.             Thread.sleep(5000);
  26.         } catch (InterruptedException e) {
  27.             e.printStackTrace();
  28.         } finally {
  29.             // 释放写锁
  30.             lock.writeLock().unlock();
  31.         }
  32.     }
  33. }
复制代码
  1. public class MyThreadTest {
  2.     public static void main(String[] args) {
  3.         Counter counter = new Counter();
  4.         Thread threadA = new Thread(new Runnable() {
  5.             @Override
  6.             public void run() {
  7.                 counter.read();
  8.             }
  9.         });
  10.         Thread threadB = new Thread(new Runnable() {
  11.             @Override
  12.             public void run() {
  13.                 counter.write();
  14.             }
  15.         });
  16.         threadA.start();
  17.         threadB.start();
  18.     }
  19. }
复制代码
看一下运行结果:
  1. 2023-10-23 16:36:08:786 当前线程:Thread-0获得了读锁,count:0
  2. 2023-10-23 16:36:13:791 当前线程:Thread-1获得了写锁,count:1
复制代码
从日志时间上可以很清晰的看到,两个线程进行串行执行,证明了读和写之间是互斥的。
三、小结

总结下来,ReadWriteLock有以下特点:

  • 允许多个线程在没有写入时同时读取,可以提高读取效率
  • 当存在写入情况时,只允许一个线程写入,其他线程进入排队等待
  • 适合读多写少的场景
对于同一个数据,有大量线程读取,但仅有少数线程修改,使用ReadWriteLock可以显著的提升程序并发执行效率。
例如,一个论坛的帖子,浏览可以看做读取操作,是非常频繁的,而回复可以看做写入操作,它是不频繁的,这种情况就可以使用ReadWriteLock来实现。
本文主要围绕ReadWriteLock的基本使用做了一次知识总结,如果有不正之处,请多多谅解,并欢迎批评指出。
四、参考

1、https://www.cnblogs.com/xrq730/p/4855631.html
2、https://www.liaoxuefeng.com/wiki/1252599548343744/1306581002092578

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

大号在练葵花宝典

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表