• 重入锁和读写锁


    一、重入锁和读写锁的引入

    Java多线程中,我们知道可以使用synchronized关键字来实现线程间的同步互斥工作,那么其实还有一个更优秀的机制去完成这个“同步互斥”工作,它就是Lock对象,我们主要学习两种锁,重入锁和读写锁。它们具有比synchronized更为强大的功能,并且有嗅探锁定、多路分支等功能。

    二、重入锁(ReentrantLock)

    重入锁(ReentrantLock)是一个java.util.concurrent.locks包下实现了Lock接口的类,如下所示:

    1. public class ReentrantLock implements Lock{
    2. ...
    3. }

    重入锁在需要进行同步的代码部分加上锁定,但不要忘记最后一定要释放锁定,不然会造成锁永远无法释放,其它线程永远进不来的结果。 

    2.1 线程t1释放锁,线程t2获得锁 

    1. import java.util.concurrent.locks.Lock;
    2. import java.util.concurrent.locks.ReentrantLock;
    3. public class UseReentrantLock {
    4. private Lock lock = new ReentrantLock();
    5. public void method1() {
    6. lock.lock();
    7. try {
    8. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入method1..");
    9. Thread.sleep(1000);
    10. System.out.println("当前线程:" + Thread.currentThread().getName() + "退出method1..");
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. } finally {
    14. lock.unlock();
    15. }
    16. }
    17. public void method2() {
    18. lock.lock();
    19. try {
    20. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入method2..");
    21. Thread.sleep(2000);
    22. System.out.println("当前线程:" + Thread.currentThread().getName() + "退出method2..");
    23. } catch (InterruptedException e) {
    24. e.printStackTrace();
    25. } finally {
    26. lock.unlock();
    27. }
    28. }
    29. public static void main(String[] args) {
    30. final UseReentrantLock ur = new UseReentrantLock();
    31. Thread t1 = new Thread(new Runnable() {
    32. @Override
    33. public void run() {
    34. ur.method1();
    35. }
    36. }, "t1");
    37. Thread t2 = new Thread(new Runnable() {
    38. @Override
    39. public void run() {
    40. ur.method2();
    41. }
    42. }, "t2");
    43. t1.start();
    44. t2.start();
    45. }
    46. }

    执行上述代码,其输出结果为:

    1. 当前线程:t1进入method1..
    2. 当前线程:t1退出method1..
    3. 当前线程:t2进入method2..
    4. 当前线程:t2退出method2..

    线程t2在执行method2的时候由于线程t1释放锁了,所以线程t2可以获取到锁,method2方法正常运行。

    2.2 线程t1未释放锁,线程t2阻塞

    像下面这种情况由于线程t1未释放锁,线程t2一直处于阻塞状态:

    1. import java.util.concurrent.locks.Lock;
    2. import java.util.concurrent.locks.ReentrantLock;
    3. public class UseReentrantLock {
    4. private Lock lock = new ReentrantLock();
    5. public void method1() {
    6. lock.lock();
    7. try {
    8. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入method1..");
    9. Thread.sleep(1000);
    10. System.out.println("当前线程:" + Thread.currentThread().getName() + "退出method1..");
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. } finally {
    14. //lock.unlock();
    15. }
    16. }
    17. public void method2() {
    18. lock.lock();
    19. try {
    20. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入method2..");
    21. Thread.sleep(2000);
    22. System.out.println("当前线程:" + Thread.currentThread().getName() + "退出method2..");
    23. } catch (InterruptedException e) {
    24. e.printStackTrace();
    25. } finally {
    26. lock.unlock();
    27. }
    28. }
    29. public static void main(String[] args) {
    30. final UseReentrantLock ur = new UseReentrantLock();
    31. Thread t1 = new Thread(new Runnable() {
    32. @Override
    33. public void run() {
    34. ur.method1();
    35. }
    36. }, "t1");
    37. Thread t2 = new Thread(new Runnable() {
    38. @Override
    39. public void run() {
    40. ur.method2();
    41. }
    42. }, "t2");
    43. t1.start();
    44. t2.start();
    45. }
    46. }

    执行上述代码,可以看到仅仅线程t1的执行结果进行了打印,线程t2由于未获得锁一直处于阻塞状态

    1. 当前线程:t1进入method1..
    2. 当前线程:t1退出method1..

    2.3 线程t1未释放锁,线程t1可以再次获得锁

    1. import java.util.concurrent.locks.Lock;
    2. import java.util.concurrent.locks.ReentrantLock;
    3. public class UseReentrantLock {
    4. private Lock lock = new ReentrantLock();
    5. public void method1() {
    6. lock.lock();
    7. try {
    8. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入method1..");
    9. Thread.sleep(1000);
    10. System.out.println("当前线程:" + Thread.currentThread().getName() + "退出method1..");
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. } finally {
    14. }
    15. }
    16. public void method2() {
    17. lock.lock();
    18. try {
    19. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入method2..");
    20. Thread.sleep(2000);
    21. System.out.println("当前线程:" + Thread.currentThread().getName() + "退出method2..");
    22. } catch (InterruptedException e) {
    23. e.printStackTrace();
    24. } finally {
    25. lock.unlock();
    26. }
    27. }
    28. public static void main(String[] args) {
    29. final UseReentrantLock ur = new UseReentrantLock();
    30. Thread t1 = new Thread(new Runnable() {
    31. @Override
    32. public void run() {
    33. ur.method1();
    34. ur.method2();
    35. }
    36. }, "t1");
    37. t1.start();
    38. }
    39. }

    执行上述代码,其输出结果为:

    1. 当前线程:t1进入method1..
    2. 当前线程:t1退出method1..
    3. 当前线程:t1进入method2..
    4. 当前线程:t1退出method2..

    可以看到线程t1在执行method1方法时虽然未释放锁,但是线程t1在执行method2时需要获得锁任然可以再次获得锁,即实现了锁重入的功能。

    三、锁与等待/通知

    还记得我们在使用synchronized的时候,如果需要多线程间进行协作工作则需要Object的wait()和notify()、notifyAlI()方法进行配合工作。那么同样,我们在使用Lock的时候,可以使用一个新的等待/通知的类,它就是Condition。这个Condition一定是针对具体某一把锁的。也就是在只有锁的基础之上才会产生Condition。

    1. import java.util.concurrent.locks.Condition;
    2. import java.util.concurrent.locks.Lock;
    3. import java.util.concurrent.locks.ReentrantLock;
    4. public class UseCondition {
    5. private Lock lock = new ReentrantLock();
    6. private Condition condition = lock.newCondition();
    7. public void method1() {
    8. try {
    9. lock.lock();
    10. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入..");
    11. Thread.sleep(3000);
    12. System.out.println("当前线程:" + Thread.currentThread().getName() + "释放锁..");
    13. condition.await(); // Object wait
    14. System.out.println("当前线程:" + Thread.currentThread().getName() + "继续执行...");
    15. } catch (Exception e) {
    16. e.printStackTrace();
    17. } finally {
    18. lock.unlock();
    19. }
    20. }
    21. public void method2() {
    22. try {
    23. lock.lock();
    24. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入..");
    25. Thread.sleep(3000);
    26. System.out.println("当前线程:" + Thread.currentThread().getName() + "发出唤醒..");
    27. condition.signal(); //Object notify
    28. } catch (Exception e) {
    29. e.printStackTrace();
    30. } finally {
    31. lock.unlock();
    32. }
    33. }
    34. public static void main(String[] args) {
    35. final UseCondition uc = new UseCondition();
    36. Thread t1 = new Thread(new Runnable() {
    37. @Override
    38. public void run() {
    39. uc.method1();
    40. }
    41. }, "t1");
    42. Thread t2 = new Thread(new Runnable() {
    43. @Override
    44. public void run() {
    45. uc.method2();
    46. }
    47. }, "t2");
    48. t1.start();
    49. t2.start();
    50. }
    51. }

    执行上述代码,其输出结果:

    1. 当前线程:t1进入..
    2. 当前线程:t1释放锁..
    3. 当前线程:t2进入..
    4. 当前线程:t2发出唤醒..
    5. 当前线程:t1继续执行...

    此处需要特别注意的是t2线程的唤醒操作是不会释放锁的,必须在调用了lock.unlock()方法以后才能释放锁。

    四、多Condition

    我们可以通过一个Lock对象产生多个Condition进行多线程间的交互,非常的灵活。可以使得部分需要唤醒的线程唤醒,其他线程则继续等待通知。

    1. import java.util.concurrent.locks.Condition;
    2. import java.util.concurrent.locks.ReentrantLock;
    3. public class UseManyCondition {
    4. private ReentrantLock lock = new ReentrantLock();
    5. private Condition c1 = lock.newCondition();
    6. private Condition c2 = lock.newCondition();
    7. public void m1(){
    8. try {
    9. lock.lock();
    10. System.out.println("当前线程:" +Thread.currentThread().getName() + "进入方法m1等待..");
    11. c1.await();
    12. System.out.println("当前线程:" +Thread.currentThread().getName() + "方法m1继续..");
    13. } catch (Exception e) {
    14. e.printStackTrace();
    15. } finally {
    16. lock.unlock();
    17. }
    18. }
    19. public void m2(){
    20. try {
    21. lock.lock();
    22. System.out.println("当前线程:" +Thread.currentThread().getName() + "进入方法m2等待..");
    23. c1.await();
    24. System.out.println("当前线程:" +Thread.currentThread().getName() + "方法m2继续..");
    25. } catch (Exception e) {
    26. e.printStackTrace();
    27. } finally {
    28. lock.unlock();
    29. }
    30. }
    31. public void m3(){
    32. try {
    33. lock.lock();
    34. System.out.println("当前线程:" +Thread.currentThread().getName() + "进入方法m3等待..");
    35. c2.await();
    36. System.out.println("当前线程:" +Thread.currentThread().getName() + "方法m3继续..");
    37. } catch (Exception e) {
    38. e.printStackTrace();
    39. } finally {
    40. lock.unlock();
    41. }
    42. }
    43. public void m4(){
    44. try {
    45. lock.lock();
    46. System.out.println("当前线程:" +Thread.currentThread().getName() + "唤醒..");
    47. c1.signalAll();
    48. } catch (Exception e) {
    49. e.printStackTrace();
    50. } finally {
    51. lock.unlock();
    52. }
    53. }
    54. public void m5(){
    55. try {
    56. lock.lock();
    57. System.out.println("当前线程:" +Thread.currentThread().getName() + "唤醒..");
    58. c2.signal();
    59. } catch (Exception e) {
    60. e.printStackTrace();
    61. } finally {
    62. lock.unlock();
    63. }
    64. }
    65. public static void main(String[] args) {
    66. final UseManyCondition umc = new UseManyCondition();
    67. Thread t1 = new Thread(new Runnable() {
    68. @Override
    69. public void run() {
    70. umc.m1();
    71. }
    72. },"t1");
    73. Thread t2 = new Thread(new Runnable() {
    74. @Override
    75. public void run() {
    76. umc.m2();
    77. }
    78. },"t2");
    79. Thread t3 = new Thread(new Runnable() {
    80. @Override
    81. public void run() {
    82. umc.m3();
    83. }
    84. },"t3");
    85. Thread t4 = new Thread(new Runnable() {
    86. @Override
    87. public void run() {
    88. umc.m4();
    89. }
    90. },"t4");
    91. Thread t5 = new Thread(new Runnable() {
    92. @Override
    93. public void run() {
    94. umc.m5();
    95. }
    96. },"t5");
    97. t1.start(); // c1
    98. t2.start(); // c1
    99. t3.start(); // c2
    100. try {
    101. Thread.sleep(2000);
    102. } catch (InterruptedException e) {
    103. e.printStackTrace();
    104. }
    105. t4.start(); // c1
    106. try {
    107. Thread.sleep(2000);
    108. } catch (InterruptedException e) {
    109. e.printStackTrace();
    110. }
    111. t5.start(); // c2
    112. }
    113. }

    执行上述代码,其输出结果:

    1. 当前线程:t1进入方法m1等待..
    2. 当前线程:t3进入方法m3等待..
    3. 当前线程:t2进入方法m2等待..
    4. 当前线程:t4唤醒..
    5. 当前线程:t1方法m1继续..
    6. 当前线程:t2方法m2继续..
    7. 当前线程:t5唤醒..
    8. 当前线程:t3方法m3继续..

    c1.signalAll()表示唤醒所有线程,c2.signal()表示随机唤醒一个线程

    五、Lock/Condition的其它用法

    公平锁与非公平锁:

    Lock lock = new ReentrantLock(boolean isFair);

    Lock用法:

    1. tryLock():尝试获得锁,根据嗅探获得结果用true/false返回,不会产生锁等待
    2. tryLock(long timeout, TimeUnit unit):在给定的时间内尝试获得锁,获得结果用truelfalse返回
    3. isFair():是否是公平锁
    4. isLocked():是否锁定
    5. getHoldCount():查询当前线程保持此锁的个数,也就是调用lock()次数
    6. lockInterruptibly():优先响应中断的锁
    7. getQueueLength():返回正在等待获取此锁定的线程数
    8. getWaitQueueLength():返回等待与锁定相关的给定条件Condition的线程数
    9. hasQueuedThread(Thread thread):查询指定的线程是否正在等待此锁
    10. hasQueuedThreads():查询是否有线程正在等待此锁
    11. hasWaiters():查询是否有线程正在等待与此锁定有关的condition条件

    六、读写锁(ReentrantReadWriteLock)

    ReadWriteLock是 java.util.concurrent.locks 包下的一个接口,ReentrantReadWriteLock是java.util.concurrent.locks 包下的一个类,ReentrantReadWriteLock(读写锁)是ReadWriteLock的一个实现类

    1. package java.util.concurrent.locks;
    2. public interface ReadWriteLock {
    3. ...
    4. }
    1. package java.util.concurrent.locks;
    2. public class ReentrantReadWriteLock implements ReadWriteLock{
    3. ...
    4. }

    读写锁ReentrantReadWriteLock,其核心就是实现读写分离的锁。在高并发访问下,尤其是读多写少的情况下,性能要远高于重入锁。

    之前学synchronized、ReentrantLock时,我们知道,同一时间内,只能有一个线程进行访问被锁定的代码,那么读写锁则不同,其本质是分成两个锁,即读锁、写锁。在读锁下,多个线程可以并发的进行访问,但是在写锁的时候,只能一个一个的顺序访问。

    口诀:读读共享,写写互斥、读写互斥

    6.1 读读共享

    1. import java.util.concurrent.locks.ReentrantReadWriteLock;
    2. import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
    3. public class UseReentrantReadWriteLock {
    4. private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    5. private ReadLock readLock = rwLock.readLock();
    6. public void read(){
    7. try {
    8. readLock.lock();
    9. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入...");
    10. Thread.sleep(3000);
    11. System.out.println("当前线程:" + Thread.currentThread().getName() + "退出...");
    12. } catch (Exception e) {
    13. e.printStackTrace();
    14. } finally {
    15. readLock.unlock();
    16. }
    17. }
    18. public static void main(String[] args) {
    19. final UseReentrantReadWriteLock urrw = new UseReentrantReadWriteLock();
    20. Thread t1 = new Thread(new Runnable() {
    21. @Override
    22. public void run() {
    23. urrw.read();
    24. }
    25. }, "t1");
    26. Thread t2 = new Thread(new Runnable() {
    27. @Override
    28. public void run() {
    29. urrw.read();
    30. }
    31. }, "t2");
    32. //多个读操作的线程可以并发的去执行
    33. t1.start();
    34. t2.start();
    35. }
    36. }

    执行上述代码,其输出内容为:

    1. 当前线程:t2进入...
    2. 当前线程:t1进入...
    3. 当前线程:t1退出...
    4. 当前线程:t2退出...

    如果是进行读操作的话,多个线程可以并发的访问

    6.2 读写互斥

    1. import java.util.concurrent.locks.ReentrantReadWriteLock;
    2. import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
    3. import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
    4. public class UseReentrantReadWriteLock {
    5. private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    6. private ReadLock readLock = rwLock.readLock();
    7. private WriteLock writeLock = rwLock.writeLock();
    8. public void read(){
    9. try {
    10. readLock.lock();
    11. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入...");
    12. Thread.sleep(3000);
    13. System.out.println("当前线程:" + Thread.currentThread().getName() + "退出...");
    14. } catch (Exception e) {
    15. e.printStackTrace();
    16. } finally {
    17. readLock.unlock();
    18. }
    19. }
    20. public void write(){
    21. try {
    22. writeLock.lock();
    23. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入...");
    24. Thread.sleep(3000);
    25. System.out.println("当前线程:" + Thread.currentThread().getName() + "退出...");
    26. } catch (Exception e) {
    27. e.printStackTrace();
    28. } finally {
    29. writeLock.unlock();
    30. }
    31. }
    32. public static void main(String[] args) {
    33. final UseReentrantReadWriteLock urrw = new UseReentrantReadWriteLock();
    34. Thread t1 = new Thread(new Runnable() {
    35. @Override
    36. public void run() {
    37. urrw.read();
    38. }
    39. }, "t1");
    40. Thread t3 = new Thread(new Runnable() {
    41. @Override
    42. public void run() {
    43. urrw.write();
    44. }
    45. }, "t3");
    46. //读写互斥
    47. t1.start();
    48. t3.start();
    49. }
    50. }

    执行上述代码,其输出结果为:

    1. 当前线程:t1进入...
    2. 当前线程:t1退出...
    3. 当前线程:t3进入...
    4. 当前线程:t3退出...

    可以看到只有当线程t1的读锁释放以后,线程t2才能获取到写锁

    6.3 写写互斥 

    1. import java.util.concurrent.locks.ReentrantReadWriteLock;
    2. import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
    3. public class UseReentrantReadWriteLock {
    4. private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    5. private WriteLock writeLock = rwLock.writeLock();
    6. public void write(){
    7. try {
    8. writeLock.lock();
    9. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入...");
    10. Thread.sleep(3000);
    11. System.out.println("当前线程:" + Thread.currentThread().getName() + "退出...");
    12. } catch (Exception e) {
    13. e.printStackTrace();
    14. } finally {
    15. writeLock.unlock();
    16. }
    17. }
    18. public static void main(String[] args) {
    19. final UseReentrantReadWriteLock urrw = new UseReentrantReadWriteLock();
    20. Thread t3 = new Thread(new Runnable() {
    21. @Override
    22. public void run() {
    23. urrw.write();
    24. }
    25. }, "t3");
    26. Thread t4 = new Thread(new Runnable() {
    27. @Override
    28. public void run() {
    29. urrw.write();
    30. }
    31. }, "t4");
    32. // 写写互斥
    33. t3.start();
    34. t4.start();
    35. }
    36. }

    执行上述代码,其输出结果为:

    1. 当前线程:t3进入...
    2. 当前线程:t3退出...
    3. 当前线程:t4进入...
    4. 当前线程:t4退出...

    可以看到只有当线程t3的读锁释放以后,线程t4才能获取到读锁

    七、锁的优化

    1、避免死锁

    2、减小锁的持有时间

    3、减小锁的粒度

    4、锁的分离

    5、尽量使用无锁的操作,如原子操作(Atomic系列类),锁升级优化

  • 相关阅读:
    SQL窗口函数
    MethodArgumentNotValidException 与 ConstraintViolationException
    flume系列之:基于zookeeper部署flume agent升级guava和curator版本
    oss 部署前端项目报错 Cannot find module ‘@/views/.....‘(require和import区别)
    旋转框/微调按钮的基类--QAbstractSpinBox 类
    核心内参: TDR原理及常见问题
    站在自动装配角度浅析Spring和SpringBoot的区别
    【网络编程】第三章 网络套接字(TCP协议程序+多进程+多线程+线程池)
    【Java数据类型】
    通过sls采集k8s集群上的服务日志
  • 原文地址:https://blog.csdn.net/y_bccl27/article/details/123884623