多线程系列(八) -ReentrantLock基本用法介绍

打印 上一主题 下一主题

主题 870|帖子 870|积分 2610

一、简介

在之前的线程系列文章中,我们介绍到了使用synchronized关键字可以实现线程同步安全的效果,以及采用wait()、notify()和notifyAll()方法,可以实现多个线程之间的通信协调,基本可以满足并发编程的需求。
但是采用synchronized进行加锁,这种锁一般都比较重,里面的实现机制也非常复杂,同时获取锁时必须一直等待,没有额外的尝试机制,如果编程不当,可能就容易发生死锁现象。
从 JDK 1.5 开始,引入了一个高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大的简化多线程程序的编写。
比如我们今天要介绍的java.util.concurrent.locks包提供的ReentrantLock类,一个可重入的互斥锁,它具有与使用synchronized加锁一样的特性,并且功能更加强大。
下面我们一起来学习一下ReentrantLock类的基本使用。
二、ReentrantLock 基本用法

在介绍ReentrantLock之前,我们先来看一下传统的使用synchronized对方法进行加锁的示例。
  1. public class Counter {
  2.     private int count;
  3.     public void add() {
  4.         synchronized(this) {
  5.             count ++;
  6.             System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
  7.         }
  8.     }
  9.     public int getCount() {
  10.         return count;
  11.     }
  12. }
复制代码
  1. public static void main(String[] args) throws InterruptedException {
  2.     Counter counter = new Counter();
  3.     // 创建5个线程,同时对count进行加一操作
  4.     for (int i = 0; i < 5; i++) {
  5.         new Thread(new Runnable() {
  6.             @Override
  7.             public void run() {
  8.                 counter.add();
  9.             }
  10.         }).start();
  11.     }
  12.     // 假设休眠1秒,5个线程执行完毕
  13.     Thread.sleep(1000);
  14.     System.out.println("count:" + counter.getCount());
  15. }
复制代码
输出结果如下:
  1. ThreadName:Thread-0, count:1
  2. ThreadName:Thread-1, count:2
  3. ThreadName:Thread-2, count:3
  4. ThreadName:Thread-3, count:4
  5. ThreadName:Thread-4, count:5
  6. count:5
复制代码
如果用ReentrantLock替代,只需要将Counter中的代码改造为如下:
  1. public class Counter {
  2.     private final Lock lock = new ReentrantLock();
  3.     private int count;
  4.     public void add() {
  5.         // 加锁
  6.         lock.lock();
  7.         try {
  8.             count ++;
  9.             System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
  10.         } finally {
  11.             // 释放锁
  12.             lock.unlock();
  13.         }
  14.     }
  15.    
  16.     public int getCount() {
  17.         return count;
  18.     }
  19. }
复制代码
运行程序,结果与上面一致,可以证明:ReentrantLock具备与synchronized一样的加锁功能。
同时,ReentrantLock还具备在指定的时间内尝试获取锁的机制,比如下面这行代码:
  1. if (lock.tryLock(3, TimeUnit.SECONDS)) {
  2.     try {
  3.         ...
  4.     } finally {
  5.         lock.unlock();
  6.     }
  7. }
复制代码
尝试在 3 秒内获取锁,如果获取不到就返回false,程序不需要无限等待下去,这个功能在实际开发中使用非常的广泛。
从上面的示例代码,我们可以总结出synchronized和ReentrantLock有以下几点不一样。

  • ReentrantLock需要手动调用加锁方法;而synchronized不需要,它采用了隐藏的加锁方式,借助 jvm 来实现
  • synchronized不需要考虑异常;而ReentrantLock获取锁之后,要在finally中正确的释放锁,否则会影响其它线程
  • ReentrantLock拥有尝试获取锁的超时机制,利用它可以避免无限等待;而synchronized不具备
  • synchronized是 Java 语言层面提供的语法;而ReentrantLock是 Java 代码实现的可重入锁
因此,在并发编程中,使用ReentrantLock比直接使用synchronized更灵活、更安全,采用tryLock(long time, TimeUnit unit)方法,即使未获取到锁也不会导致死锁。
三、ReentrantLock 和 synchronized 持有的对象监视器是同一个吗?

可能有的同学会发出这样的一个问题,使用ReentrantLock进行加锁和使用synchronized加锁,两者持有的对象监视器是同一个吗?
下面我们一起来看一个例子。
  1. public class Counter {
  2.     private final Lock lock = new ReentrantLock();
  3.     private int count;
  4.     public synchronized void methodA() {
  5.         System.out.println("ThreadName:" + Thread.currentThread().getName() + ",begin methodA, count:" + getCount());
  6.         try {
  7.             Thread.sleep(3000);
  8.         } catch (InterruptedException e) {
  9.             e.printStackTrace();
  10.         }
  11.         count ++;
  12.         System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
  13.     }
  14.     public void methodB() {
  15.         // 加锁
  16.         lock.lock();
  17.         try {
  18.             System.out.println("ThreadName:" + Thread.currentThread().getName() + ",begin methodB, count:" + getCount());
  19.             Thread.sleep(3000);
  20.             count ++;
  21.             System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
  22.         } catch (Exception e){
  23.           e.printStackTrace();
  24.         } finally {
  25.             // 释放锁
  26.             lock.unlock();
  27.         }
  28.     }
  29.     public int getCount() {
  30.         return count;
  31.     }
  32. }
复制代码
  1. public class MyThreadA extends Thread {
  2.     private Counter counter;
  3.     public MyThreadA(Counter counter) {
  4.         this.counter = counter;
  5.     }
  6.     @Override
  7.     public void run() {
  8.         counter.methodA();
  9.     }
  10. }
复制代码
  1. public class MyThreadB extends Thread {
  2.     private Counter counter;
  3.     public MyThreadB(Counter counter) {
  4.         this.counter = counter;
  5.     }
  6.     @Override
  7.     public void run() {
  8.         counter.methodB();
  9.     }
  10. }
复制代码
  1. public class MyThreadTest {
  2.     public static void main(String[] args) {
  3.         Counter counter = new Counter();
  4.         MyThreadA threadA = new MyThreadA(counter);
  5.         threadA.start();
  6.         MyThreadB threadB = new MyThreadB(counter);
  7.         threadB.start();
  8.     }
  9. }
复制代码
看一下运行结果:
  1. ThreadName:Thread-0,begin methodA, count:0
  2. ThreadName:Thread-1,begin methodB, count:0
  3. ThreadName:Thread-0, count:2
  4. ThreadName:Thread-1, count:2
复制代码
从日志上可以看出,采用两个线程分别采用synchronized和ReentrantLock两种加锁方式对count进行操作,两个线程交替执行,可以得出一个结论:synchronized和ReentrantLock持有的对象监视器不同。
四、Condition 基本用法

在之前的文章中,我们介绍了在synchronized同步方法/代码块中,使用wait()、notify()和notifyAll()可以实现线程之间的等待/通知模型。
ReentrantLock同样也可以,只需要借助Condition类即可实现,Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的。
我们还是先来看一个简单的示例。
  1. public class Counter {
  2.     private final Lock lock = new ReentrantLock();
  3.     private Condition condition = lock.newCondition();
  4.     private int count;
  5.     public void await(){
  6.         // 加锁
  7.         lock.lock();
  8.         try {
  9.             condition.await();
  10.             System.out.println("await等待结束,count:" + getCount());
  11.         } catch (Exception e){
  12.             e.printStackTrace();
  13.         } finally {
  14.             // 释放锁
  15.             lock.unlock();
  16.         }
  17.     }
  18.     public void signal(){
  19.         // 加锁
  20.         lock.lock();
  21.         try {
  22.             count++;
  23.             // 唤醒某个等待线程
  24.             condition.signal();
  25.             // 唤醒所有等待线程
  26. //            condition.signalAll();
  27.             System.out.println("signal 唤醒通知完毕");
  28.         } catch (Exception e){
  29.             e.printStackTrace();
  30.         } finally {
  31.             // 释放锁
  32.             lock.unlock();
  33.         }
  34.     }
  35.     public int getCount() {
  36.         return count;
  37.     }
  38. }
复制代码
  1. public class MyThreadA extends Thread {
  2.     private Counter counter;
  3.     public MyThreadA(Counter counter) {
  4.         this.counter = counter;
  5.     }
  6.     @Override
  7.     public void run() {
  8.         counter.await();
  9.     }
  10. }
复制代码
  1. public class MyThreadB extends Thread {
  2.     private Counter counter;
  3.     public MyThreadB(Counter counter) {
  4.         this.counter = counter;
  5.     }
  6.     @Override
  7.     public void run() {
  8.         counter.signal();
  9.     }
  10. }
复制代码
  1. public class MyThreadTest {
  2.     public static void main(String[] args) throws InterruptedException {
  3.         Counter counter = new Counter();
  4.         // 先启动执行等待的线程
  5.         MyThreadA threadA = new MyThreadA(counter);
  6.         threadA.start();
  7.         Thread.sleep(3000);
  8.         // 过3秒,再启动执行通知的线程
  9.         MyThreadB threadB = new MyThreadB(counter);
  10.         threadB.start();
  11.     }
  12. }
复制代码
看一下运行结果:
  1. signal 通知完毕
  2. await等待结束,count:1
复制代码
从结果上看很明显的看出,等待线程MyThreadA先启动,过了 3 秒之后再启动了MyThreadB,但是signal()方法先执行完毕,再通知await()方法执行,符合代码预期。
这个例子也证明了一点:condition.await()方法是释放了锁,不然signal()方法体不会被执行。
相比wait/notify/notifyAll的等待/通知模型,Condition更加灵活,理由有以下几点:

  • notify()方法唤醒线程时,被通知的线程由 Java 虚拟机随机选择;而采用ReentrantLock结合Condition可以实现有选择性地通知,这一特性在实际编程中非常实用
  • 一个Lock里面可以创建多个Condition实例,实现多路通知,使用多个Condition的应用场景很常见,比如ArrayBlockingQueue
五、小结

本文主要围绕ReentrantLock的基本使用做了一次简单的知识总结,如果有不正之处,请多多谅解,并欢迎批评指出。
六、参考

1、博客园 -五月的仓颉 - ReentrantLock的使用和Condition
2、廖雪峰 - 使用ReentrantLock
七、写到最后

最近无意间获得一份阿里大佬写的技术笔记,内容涵盖 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL 等技术知识。需要的小伙伴可以点击如下链接获取,资源地址:技术资料笔记

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!

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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

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

本版积分规则

瑞星

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

标签云

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