【Java并发】ReadWriteLock读写锁的使用

左手的ㄟ右手 2023-06-27 06:29 36阅读 0赞

本文转自https://www.jianshu.com/p/9cd5212c8841 ,主要用于自己学习使用。

主要内容如下:

说到Java并发编程,很多开发第一个想到同时也是经常常用的肯定是Synchronized,但是小编这里提出一个问题,Synchronized存在明显的一个性能问题就是读与读之间互斥,简言之就是,我们编程想要实现的最好效果是,可以做到读和读互不影响,读和写互斥,写和写互斥,提高读写的效率,如何实现呢?

Java并发包中ReadWriteLock是一个接口,主要有两个方法,如下:

  1. public interface ReadWriteLock {
  2. /**
  3. * Returns the lock used for reading.
  4. *
  5. * @return the lock used for reading
  6. */
  7. Lock readLock();
  8. /**
  9. * Returns the lock used for writing.
  10. *
  11. * @return the lock used for writing
  12. */
  13. Lock writeLock();
  14. }

ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。
Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。
在具体讲解ReetrantReadWriteLock的使用方法前,我们有必要先对其几个特性进行一些深入学习了解。

1. ReetrantReadWriteLock特性说明

1.1 获取锁顺序

  • 非公平模式(默认)
    当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。
  • 公平模式
    当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。

1.2 可重入

什么是可重入锁,不可重入锁呢?”重入”字面意思已经很明显了,就是可以重新进入。可重入锁,就是说一个线程在获取某个锁后,还可以继续获取该锁,即允许一个线程多次获取同一个锁。比如synchronized内置锁就是可重入的,如果A类有2个synchornized方法method1和method2,那么method1调用method2是允许的。显然重入锁给编程带来了极大的方便。假如内置锁不是可重入的,那么导致的问题是:1个类的synchornized方法不能调用本类其他synchornized方法,也不能调用父类中的synchornized方法。与内置锁对应,JDK提供的显示锁ReentrantLock也是可以重入的,这里通过一个例子着重说下可重入锁的释放需要的事儿。

  1. package test;
  2. import java.util.concurrent.locks.ReentrantReadWriteLock;
  3. public class Test1 {
  4. public static void main(String[] args) throws InterruptedException {
  5. final ReentrantReadWriteLock lock = new ReentrantReadWriteLock ();
  6. Thread t = new Thread(new Runnable() {
  7. @Override
  8. public void run() {
  9. lock.writeLock().lock();
  10. System.out.println("Thread real execute");
  11. lock.writeLock().unlock();
  12. }
  13. });
  14. lock.writeLock().lock();
  15. lock.writeLock().lock();
  16. t.start();
  17. Thread.sleep(200);
  18. System.out.println("realse one once");
  19. lock.writeLock().unlock();
  20. }
  21. }

format_png

运行结果.png

从运行结果中,可以看到,程序并未执行线程的run方法,由此我们可知,上面的代码会出现死锁,因为主线程2次获取了锁,但是却只释放1次锁,导致线程t永远也不能获取锁。一个线程获取多少次锁,就必须释放多少次锁。这对于内置锁也是适用的,每一次进入和离开synchornized方法(代码块),就是一次完整的锁获取和释放。

format_png 1

format_png 1再次添加一次unlock之后的运行结果.png

1.3 锁降级

要实现一个读写锁,需要考虑很多细节,其中之一就是锁升级和锁降级的问题。什么是升级和降级呢?ReadWriteLock的javadoc有一段话:

Can the write lock be downgraded to a read lock without allowing an intervening writer? Can a read lock be upgraded to a write lock, in preference to other waiting readers or writers?

翻译过来的结果是:在不允许中间写入的情况下,写入锁可以降级为读锁吗?读锁是否可以升级为写锁,优先于其他等待的读取或写入操作?简言之就是说,锁降级:从写锁变成读锁;锁升级:从读锁变成写锁,ReadWriteLock是否支持呢?让我们带着疑问,进行一些Demo 测试代码验证。

Test Code 1

  1. /**
  2. *Test Code 1
  3. **/
  4. package test;
  5. import java.util.concurrent.locks.ReentrantReadWriteLock;
  6. public class Test1 {
  7. public static void main(String[] args) {
  8. ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
  9. rtLock.readLock().lock();
  10. System.out.println("get readLock.");
  11. rtLock.writeLock().lock();
  12. System.out.println("blocking");
  13. }
  14. }

Test Code 1 Result

format_png 2

TestCode1 Result.png

结论:上面的测试代码会产生死锁,因为同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock是不支持的

Test Code 2

  1. /**
  2. *Test Code 2
  3. **/
  4. package test;
  5. import java.util.concurrent.locks.ReentrantReadWriteLock;
  6. public class Test2 {
  7. public static void main(String[] args) {
  8. ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
  9. rtLock.writeLock().lock();
  10. System.out.println("writeLock");
  11. rtLock.readLock().lock();
  12. System.out.println("get read lock");
  13. }
  14. }

Test Code 2 Result

format_png 3

TestCode2 Result.png

结论:ReentrantReadWriteLock支持锁降级,上面代码不会产生死锁。这段代码虽然不会导致死锁,但没有正确的释放锁。从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。

2. ReetrantReadWriteLock对比使用

2.1 Synchronized实现

在使用ReetrantReadWriteLock实现锁机制前,我们先看一下,多线程同时读取文件时,用synchronized实现的效果

  1. package test;
  2. /**
  3. *
  4. * synchronized实现
  5. * @author itbird
  6. *
  7. */
  8. public class ReadAndWriteLockTest {
  9. public synchronized static void get(Thread thread) {
  10. System.out.println("start time:" + System.currentTimeMillis());
  11. for (int i = 0; i < 5; i++) {
  12. try {
  13. Thread.sleep(20);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println(thread.getName() + ":正在进行读操作……");
  18. }
  19. System.out.println(thread.getName() + ":读操作完毕!");
  20. System.out.println("end time:" + System.currentTimeMillis());
  21. }
  22. public static void main(String[] args) {
  23. new Thread(new Runnable() {
  24. @Override
  25. public void run() {
  26. get(Thread.currentThread());
  27. }
  28. }).start();
  29. new Thread(new Runnable() {
  30. @Override
  31. public void run() {
  32. get(Thread.currentThread());
  33. }
  34. }).start();
  35. }
  36. }

让我们看一下运行结果:

format_png 4

synchronized实现的效果结果.png

从运行结果可以看出,两个线程的读操作是顺序执行的,整个过程大概耗时200ms。

2.2 ReetrantReadWriteLock实现

  1. package test;
  2. import java.util.concurrent.locks.ReentrantReadWriteLock;
  3. /**
  4. *
  5. * ReetrantReadWriteLock实现
  6. * @author itbird
  7. *
  8. */
  9. public class ReadAndWriteLockTest {
  10. public static void get(Thread thread) {
  11. ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
  12. lock.readLock().lock();
  13. System.out.println("start time:" + System.currentTimeMillis());
  14. for (int i = 0; i < 5; i++) {
  15. try {
  16. Thread.sleep(20);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. System.out.println(thread.getName() + ":正在进行读操作……");
  21. }
  22. System.out.println(thread.getName() + ":读操作完毕!");
  23. System.out.println("end time:" + System.currentTimeMillis());
  24. lock.readLock().unlock();
  25. }
  26. public static void main(String[] args) {
  27. new Thread(new Runnable() {
  28. @Override
  29. public void run() {
  30. get(Thread.currentThread());
  31. }
  32. }).start();
  33. new Thread(new Runnable() {
  34. @Override
  35. public void run() {
  36. get(Thread.currentThread());
  37. }
  38. }).start();
  39. }
  40. }

让我们看一下运行结果:

format_png 5

ReetrantReadWriteLock实现.png

从运行结果可以看出,两个线程的读操作是同时执行的,整个过程大概耗时100ms。
通过两次实验的对比,我们可以看出来,ReetrantReadWriteLock的效率明显高于Synchronized关键字。

3. ReetrantReadWriteLock读写锁互斥关系

通过上面的测试代码,我们也可以延伸得出一个结论,ReetrantReadWriteLock读锁使用共享模式,即:同时可以有多个线程并发地读数据。但是另一个问题来了,写锁之间是共享模式还是互斥模式?读写锁之间是共享模式还是互斥模式呢?下面让我们通过Demo进行一一验证吧。

3.1 ReetrantReadWriteLock读写锁关系

  1. package test;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. import java.util.concurrent.locks.ReentrantReadWriteLock;
  5. /**
  6. *
  7. * ReetrantReadWriteLock实现
  8. * @author itbird
  9. *
  10. */
  11. public class ReadAndWriteLockTest {
  12. public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
  13. public static void main(String[] args) {
  14. //同时读、写
  15. ExecutorService service = Executors.newCachedThreadPool();
  16. service.execute(new Runnable() {
  17. @Override
  18. public void run() {
  19. readFile(Thread.currentThread());
  20. }
  21. });
  22. service.execute(new Runnable() {
  23. @Override
  24. public void run() {
  25. writeFile(Thread.currentThread());
  26. }
  27. });
  28. }
  29. // 读操作
  30. public static void readFile(Thread thread) {
  31. lock.readLock().lock();
  32. boolean readLock = lock.isWriteLocked();
  33. if (!readLock) {
  34. System.out.println("当前为读锁!");
  35. }
  36. try {
  37. for (int i = 0; i < 5; i++) {
  38. try {
  39. Thread.sleep(20);
  40. } catch (InterruptedException e) {
  41. e.printStackTrace();
  42. }
  43. System.out.println(thread.getName() + ":正在进行读操作……");
  44. }
  45. System.out.println(thread.getName() + ":读操作完毕!");
  46. } finally {
  47. System.out.println("释放读锁!");
  48. lock.readLock().unlock();
  49. }
  50. }
  51. // 写操作
  52. public static void writeFile(Thread thread) {
  53. lock.writeLock().lock();
  54. boolean writeLock = lock.isWriteLocked();
  55. if (writeLock) {
  56. System.out.println("当前为写锁!");
  57. }
  58. try {
  59. for (int i = 0; i < 5; i++) {
  60. try {
  61. Thread.sleep(20);
  62. } catch (InterruptedException e) {
  63. e.printStackTrace();
  64. }
  65. System.out.println(thread.getName() + ":正在进行写操作……");
  66. }
  67. System.out.println(thread.getName() + ":写操作完毕!");
  68. } finally {
  69. System.out.println("释放写锁!");
  70. lock.writeLock().unlock();
  71. }
  72. }
  73. }

运行结果:

format_png 6

运行结果.png

结论:读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容,读写锁之间为互斥。

3.2 ReetrantReadWriteLock写锁关系

  1. package test;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. import java.util.concurrent.locks.ReentrantReadWriteLock;
  5. /**
  6. *
  7. * ReetrantReadWriteLock实现
  8. * @author itbird
  9. *
  10. */
  11. public class ReadAndWriteLockTest {
  12. public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
  13. public static void main(String[] args) {
  14. //同时写
  15. ExecutorService service = Executors.newCachedThreadPool();
  16. service.execute(new Runnable() {
  17. @Override
  18. public void run() {
  19. writeFile(Thread.currentThread());
  20. }
  21. });
  22. service.execute(new Runnable() {
  23. @Override
  24. public void run() {
  25. writeFile(Thread.currentThread());
  26. }
  27. });
  28. }
  29. // 读操作
  30. public static void readFile(Thread thread) {
  31. lock.readLock().lock();
  32. boolean readLock = lock.isWriteLocked();
  33. if (!readLock) {
  34. System.out.println("当前为读锁!");
  35. }
  36. try {
  37. for (int i = 0; i < 5; i++) {
  38. try {
  39. Thread.sleep(20);
  40. } catch (InterruptedException e) {
  41. e.printStackTrace();
  42. }
  43. System.out.println(thread.getName() + ":正在进行读操作……");
  44. }
  45. System.out.println(thread.getName() + ":读操作完毕!");
  46. } finally {
  47. System.out.println("释放读锁!");
  48. lock.readLock().unlock();
  49. }
  50. }
  51. // 写操作
  52. public static void writeFile(Thread thread) {
  53. lock.writeLock().lock();
  54. boolean writeLock = lock.isWriteLocked();
  55. if (writeLock) {
  56. System.out.println("当前为写锁!");
  57. }
  58. try {
  59. for (int i = 0; i < 5; i++) {
  60. try {
  61. Thread.sleep(20);
  62. } catch (InterruptedException e) {
  63. e.printStackTrace();
  64. }
  65. System.out.println(thread.getName() + ":正在进行写操作……");
  66. }
  67. System.out.println(thread.getName() + ":写操作完毕!");
  68. } finally {
  69. System.out.println("释放写锁!");
  70. lock.writeLock().unlock();
  71. }
  72. }
  73. }

运行结果:

format_png 7

运行结果.png

4. 总结

1.Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性
2.ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字
3.ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的
4.ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁

发表评论

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

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

相关阅读

    相关 ReadWriteLock

    现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如

    相关 ReadWriteLock

           隐式锁Synchronized、重入锁ReetrantLock都是互斥锁、独占锁,即同一个锁只能每时每刻至多由一个线程来获持有。互斥,是一种保守策略,虽然避免了“

    相关 ReadWriteLock

    首先对于一个技术,存在就是为了解决某些技术难点。 为什么已经有ReentLock锁,却还要引入读写锁呢? 答案就是为了解决在 读多写少的场景下的性能问题,运用读写锁,能提高