编程之路

  • home
  • article
  • class
  • label
  • utils

  • 搜索
Elasticsearch MongoDB 衡量点 aop 边缘计算 框架 物联网 敏捷开发 团队 压力测试 Markdown 学习方法 学习 产品 规范 日志 微服务 壁纸 开发平台 Java 全栈 前端 开发规范 MQTT CentOS 镜像站 IntelliJ IDEA FreeMarker UML 计算机 软件 Tomcat Netty Web Service Docker Dubbo Kafka NoSQL Redis 消息队列 RocketMQ RabbitMQ ActiveMQ 分布式事务 Spring 队列 Java 高级 GC JVM HTTP 网络安全 算法 设计模式 Spring Cloud Web SpringMVC 线程池 并发 锁🔒 多线程 Git Java 集合 Java 基础 MyBatis 数据库 MySQL Java 基础面试题 Java Nginx Linux Spring Boot

synchronized 的替代品 ReentrantLock

发表于 2021-12-21 | 分类于 Java | 0 | 阅读次数 938

本来这篇文章打算写下细粒度锁的几种通用实现的,但在实践的过程中,我觉得有必要先介绍一下 ReentrantLock 这个类,可能大部分人都没有使用过,其实我也是一样,在接触到这个类之前都是只用过 synchronized 关键字,直到接触到了 ReentrantLock 这个类才知道还有这个东西,哎,还是对并发编程不是非常熟悉,对 JUC 并发包还需要进一步的去学习,下面我们就一起来看下 ReentrantLock 这个类是怎样的。

在 Java 中当我们需要对某个方法或代码块进行加锁时,往往我们第一时间想到的是 synchronized 关键字,通过 synchronized 来保证多线程环境下的线程安全。比如说下面两段代码:

private synchronized void syncMethod(){
  System.out.println("execute sync method");
}
private void syncMethod(){
  System.out.println("enter sync method");
  synchronized (this){
    System.out.println("execute sync method");
  }
}

上面两段代码很简单,一个是在 syncMethod 方法上加上 synchronized 关键字,另一个是在方法内部对部分代码块进行加锁,或许我们都知道这样都能保证 syncMethod 方法或 syncMethod 方法内部分代码块是线程安全的,那么有没有另外一种方式来实现这一效果呢?

答案是有的,也就是我们上面提到的 ReentrantLock 类了,他可以说是 synchronized 的替代品,一般翻译成再入锁,和 synchronized 关键字一样都是可重入的,也就是说当一个线程尝试获取它已经获取成功的锁时,这时锁直接获取成功。那我们先看如何使用 ReentrantLock 类实现上面的效果吧。

其实第一种也可以认为是使用 synchronized 关键字将整个 syncMethod 方法的代码包裹起来,就像下面这样:

private void syncMethod(){
  synchronized (this){
    System.out.println("execute sync method");
  }
}

使用 ReentrantLock 类实现加锁方式如下:

ReentrantLock lock = new ReentrantLock();
private void syncMethod(){
  lock.lock();
  try {
    System.out.println("execute sync method");
  }finally {
    lock.unlock();
  }
}

可以看到使用 ReentrantLock 来加锁的话其实就跟使用普通对象一样,调用 lock 方法加锁,unlock 方法解锁,只不过千万需要注意的是在我们调用 lock 方法进行加锁之后一定要保证有解锁的操作。一般我们是通过 try-finally 的方式来确保 unlock 方法被调用,而且 unlock 方法的调用需要放在 finally 代码块的第一行。还有就是 lock 方法的调用最好不要放到 try 代码块中,不然万一 lock 方法加锁失败,最终 finally 代码块中的解锁方法被调用会抛出异常,因为是在锁根本没有加成功的情况下去解锁。

如果说仅仅是作为 synchronized 关键字的替代品那就太弱了,ReentrantLock 还能够实现一些 synchronized 无法做到的场景,比如说带超时的加锁操作。有时候由于业务需要,在尝试进入加锁的代码块时,如果 3s 之内没有获取到锁的话直接返回,提示用户重新操作,这可以有效减小高并发给系统带来的压力。

private void syncMethod() {
  if(lock.tryLock(3, TimeUnit.SECONDS)){
    try {
      System.out.println("execute sync method");
      TimeUnit.SECONDS.sleep(5);
    } catch (Exception e){
      e.printStackTrace();
    } finally {
      lock.unlock();
    }
  }else{
    System.out.println("execute other logic");
  }
}

上面的代码中通过调用 ReentrantLock 类的 tryLock 带超时时间参数的方法,在 3s 内加锁失败的话执行其他逻辑。

对于 ReentrantLock 类,它还提供保证获取锁公平性,判断是否有其他线程也在排队等待获取锁等一些特性。

// 创建公平性锁
ReentrantLock lock = new ReentrantLock(true);
// 队列中是否有线程在等待获取锁
lock.hasQueuedThreads();
// 锁是否已经被线程持有
lock.isLocked();
// 是否是当前线程持有该锁
lock.isHeldByCurrentThread();

上面锁在创建的时候设置 boolean 参数指定是否需要提供公平性,一般是不太建议引入公平性锁,因为只要引入公平性锁必然就要有额外的开销来保证线程获取锁的公平性,所以说除非业务实在是需要一般不引入。

接下来我们再看一下和 ReentrantLock 类有关的一个比较实用的功能,配合 Condition 条件变量非常优雅的实现线程间的通信操作,就有点类似于 Object 类中的 wait,notify/notifyAll 方法,都是实现线程间的通信,而这里是采用 await,signal/signalAll 组合,我们可以看下面一段很简单的代码:

private List<String> list = new ArrayList<>();
private ReentrantLock reentrantLock = new ReentrantLock();
private Condition notEmpty = reentrantLock.newCondition();

private void produce(String str) throws InterruptedException {
  reentrantLock.lock();
  try {
    list.add(str);
    System.out.println("send a signal to add elements to the list");
    notEmpty.signal();
    TimeUnit.SECONDS.sleep(5);
  }finally {
    reentrantLock.unlock();
    System.out.println("producer unlock");
  }
}

private void consume() throws InterruptedException {
  reentrantLock.lock();
  try {
    if(list.size() == 0){
      System.out.println("waiting for elements to add the list");
      notEmpty.await();
      System.out.println("received a signal to add elements to the list");
    }
    list.remove(list.size() - 1);
    System.out.println("consume list element");
    TimeUnit.SECONDS.sleep(1);
  }finally {
    reentrantLock.unlock();
    System.out.println("consumer unlock");
  }
}

上面代码我们拿一个 list 来简单模仿队列的生产和消费操作,同时通过 ReentrantLock 创建一个 notEmpty 的 condition 来实现当 list 集合大小为 0 时,消费端则需要等待生产端生产元素,一旦生产端有元素进入,立马通过 notEmpty 的 condition 来通知消费端消费,消费端在收到生产端的信号之后则可以继续消费。

测试代码如下:

Thread consumer = new Thread(() -> {
  try {
    test.consume();
  }catch (Exception e){
    e.printStackTrace();
  }
});

Thread producer = new Thread(() -> {
  try {
    test.produce("string");
  }catch (Exception e){
    e.printStackTrace();
  }
});

consumer.start();
TimeUnit.SECONDS.sleep(1);
producer.start();

测试代码中先启动消费线程,消费线程启动之后,等待元素进入集合,接着启动生产线程,生产元素进入集合,紧接着发送有元素加入的信号通知消费线程开始消费。测试结果如下:

waiting for elements to add the list
send a signal to add elements to the list
producer unlock
received a signal to add elements to the list
consume list element
consumer unlock

当然上面例子中的代码可能不是很严瑾,但我觉得足以说明 ReentrantLock 配合 Condition 一起使用可以实现的功能了,如果你想看下标准的实现方式,可以去看下标准类库中的 ArrayBlockingQueue,它里面的入队和出队操作就是利用了上面例子所表达的这一功能。

上面描述了 ReentrantLock 类的整体使用,基本上能够做到 synchronized 关键字所能做到功能,那么它们两者的性能如何呢?其实在 Java 6 之前的版本,synchronized 关键字的性能表现不是很理想,直到在 Java 6 中进行了很大的改进,加入了偏斜锁,轻量级锁,重量级锁实现,这才让 synchronized 关键字的性能得到很大的改善,在这之后,如果是并发冲突很高的情况下,ReentrantLock 的性能表现会更好点,相反,synchronized 关键字的表现会更好些。

# 多线程 # 锁🔒 # 并发 # Java 高级
Git 版本管理规范
AOP技术的几种实现方式
  • 文章目录
  • 站点概览
Adrian

Adrian

曙光在头上,不抬起头,便永远只能看见物质的闪光。

120 日志
11 分类
70 标签
RSS
Creative Commons
Links
  • 美团技术团队
  • 阮一峰
  • 程序猿DD
  • SpringBoot 中文社区
  • 在线文档
  • Bean Searcher
  • OkHttps
  • Grails
  • Sa-Token
  • 程序员的进击之路
  • bugstack 虫洞栈
  • Java 全栈知识体系
  • Gobrs-Async
  • 查询网
  • 微信开放社区
  • 物联网技术指南
  • emqx
  • 看云
  • 深圳核酸检测点查询
  • Hutool
  • Spring
  • V2EX
  • v-charts
  • Vert.x 官方文档
  • Vert.x 官方文档中文翻译
  • 极客时间
  • Apache RocketMQ 开发者指南
  • 知了
  • 阿里云知行动手实验室
  • Learn Git Branching
  • Spring Boot 教程
  • 未读代码
  • 如梦技术
  • jpom
  • Cubic
  • Easy-Es
  • bing-wallpaper
  • solon
  • LuatOS
  • ThingsBoard
  • Linux 中国◆开源社区
  • Apache Dubbo
  • Jenkins
  • 技术文章摘抄
  • VueJS
  • MapStruct
  • elasticsearch 中文社区
  • Apollo(阿波罗)
  • TiKV文档
  • Chrome插件分享
  • 一步步搭建物联网系统(教你设计物联网系统)
  • 全栈增长工程师指南
  • 程序员的自我修养
  • Pro Git(中文版)
  • 学习 Web 开发
  • 极客教程
  • PingCAP 文档中心
  • 酷壳
  • Refactoring Guru 网站
  • 学习 Java 语言
  • smart-doc
  • mybatis-plus
  • 字母哥博客
0%
© 2023 Adrian
由 Halo 强力驱动
|
主题 - NexT.Gemini v5.1.4