写点什么

可重入锁 ReentrantLock 在性能测试常见用法

作者:FunTester
  • 2023-10-24
    河北
  • 本文字数:2962 字

    阅读完需:约 10 分钟

在进行 Java 多线程编程的过程中,始终绕不开一个问题:线程安全。一般来说,我们可以通过对一些资源加锁来实现,大多都是通过 synchronized 关键字实现。


在做性能测试时,如果 TPS 或者 QPS 要求没有特别高, synchronized 一招鲜基本也能满足大部分的需求了。


对于一招鲜无法很好解决的问题,就需要我们继续探索 java.util.concurrent 包的其他内容。今天就分享一下 java.util.concurrent.locks.Lock 接口的实现类 java.util.concurrent.locks.ReentrantLock 的基本使用方法。

类功能概览

java.util.concurrent.locks.Lock 接口支持三种方法的锁获取:阻塞锁、可中断锁和超时锁。


下面来分享这几种锁的常用的使用场景和案例。

阻塞锁

方法是:java.util.concurrent.locks.ReentrantLock#lock,没有参数。该方法会尝试获取锁。当无法获取锁时,当前线程会处于休眠状态,直到获取锁成功。


演示 Demo 如下:


private static final Logger log = LogManager.getLogger(LockTest.class);    public static void main(String[] args) throws InterruptedException {      ReentrantLock lock = new ReentrantLock();      Thread lockTestThread = new Thread(() -> {          lock.lock();          log.info("获取到锁了!");          lock.unlock();      });      lock.lock();      lockTestThread.start();     log.info("即将马上释放锁!");     Thread.sleep(1000);      lock.unlock();      lockTestThread.join();  }
复制代码


控制台打印:


19:43:29 046 main 即将马上释放锁!19:43:30 050 Thread-2 获取到锁了!19:43:30 uptime:1 s
复制代码


由于异步线程获取锁的方法晚于 main 线程,所以会在获取锁的地方阻塞,直至 main 线程将锁释放。可以看到,两条打印日志相差约 1s。

可中断锁

可中断锁 API 是:java.util.concurrent.locks.ReentrantLock#lockInterruptibly。该方式会尝试获取锁,并且是阻塞的,但当未获取到锁时,如果当前线程被设置了中断状态,则会抛出 java.lang.InterruptedException 异常。


演示 Demo 如下:



private static final Logger log = LogManager.getLogger(LockTest.class); public static void main(String[] args) throws InterruptedException { ReentrantLock lock = new ReentrantLock(); Thread lockTestThread = new Thread(() -> { try { lock.lockInterruptibly(); log.info("获取到锁了!"); lock.unlock(); } catch (InterruptedException e) { log.warn("获取锁失败!", e); } }); lock.lock(); lockTestThread.start(); lockTestThread.interrupt(); lock.unlock(); lockTestThread.join(); }
复制代码


控制台打印:


19:58:21 250 Thread-2 获取锁失败!java.lang.InterruptedException: null  at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1220) ~[?:1.8.0_281]  at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) ~[?:1.8.0_281]  at com.funtest.temp.LockTest.lambda$main$0(LockTest.java:18) ~[classes/:?]  at java.lang.Thread.run(Thread.java:748) [?:1.8.0_281]
复制代码

超时锁

超时锁的 API 有两个:java.util.concurrent.locks.ReentrantLock#tryLock()java.util.concurrent.locks.ReentrantLock#tryLock(long, java.util.concurrent.TimeUnit),返回 1 个 Boolean 值,表示获取锁是否成功。第二个 API 参数设置超时时间。这两个 API 前者可以简单理解为后者时间设置为 0,获取一下试试,成不成都返回结果。


演示 Demo 如下:


private static final Logger log = LogManager.getLogger(LockTest.class);    public static void main(String[] args) throws InterruptedException {      ReentrantLock lock = new ReentrantLock();      Thread lockTestThread = new Thread(() -> {          boolean b = lock.tryLock();          log.info("第一次获取锁的结果:{}", b);          try {              boolean b1 = lock.tryLock(3, TimeUnit.SECONDS);              log.info("第二次获取锁的结果:{}", b1);          } catch (InterruptedException e) {              log.warn("第二次获取锁的时候被中断了");          }      });      lock.lock();      lockTestThread.start();      Thread.sleep(1000);      lock.unlock();      lockTestThread.join();  }
复制代码


控制台打印:


20:05:13 559 Thread-2 第一次获取锁的结果:false20:05:14 563 Thread-2 第二次获取锁的结果:true20:05:14 uptime:2 s
复制代码


可以看到再等待了 1s 之后,第二次获取锁成功了。为了简化代码,我并没有写判断获取锁状态的代码。

最佳实践

对于 java.util.concurrent.locks.ReentrantLock ,常用最佳实践只有一个,非常容易掌握。那就是使用 try-catch-finally 语法实现,演示 Demo 如下:


boolean status = false;  try {      status = lock.tryLock(3, TimeUnit.SECONDS);  } catch (Exception e) {      // 异常处理  } finally {      if (status) lock.unlock();  }
复制代码


  1. 尽量使用超时锁

  2. 尽可能少占用锁

  3. 尽量低频使用

可重入

java.util.concurrent.locks.ReentrantLock 直译就是可重入锁,意思是当一个线程获取到锁之后,还可以再获取一次,当然释放也需要两次。在内部有专门用来计数的功能,当然也是线程安全的。


在性能测试实践中,很少能遇到使用 可重入 的特性的场景。所以这里建议不要过度使用 java.util.concurrent.locks.ReentrantLock,复杂场景可以有更加简单可靠的解决方案。

公平锁与非公平锁

java.util.concurrent.locks.ReentrantLock 有一个构造方法,如下:


/**   * Creates an instance of {@code ReentrantLock} with the   * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy   */public ReentrantLock(boolean fair) {      sync = fair ? new FairSync() : new NonfairSync();  }
复制代码


方法参数中 Boolean 值,含义既是是否使用公平锁。无参的构造方法默认使用的非公平锁。公平锁和非公平锁的主要区别是获取锁的方式不同。公平锁的获取是公平的,线程依次排队获取锁。谁等待的时间最长,就由谁获得锁。非公平锁获取是随机的,谁先请求谁先获得锁,不一定按照请求锁的顺序来。


具体区别如下:


  1. 获取锁的方式不同


  • 公平锁:线程依次排队获取锁,效率较低

  • 非公平锁:随机获取锁,效率较高


  1. 性能不同


  • 公平锁:一次性唤醒队列中等待时间最久的线程,Context Switching 次数高,性能较低

  • 非公平锁:随机唤醒线程,Context Switching 次数低,性能较高


  1. 锁等待时间


  • 公平锁:等待时间长,但访问顺序按队列顺序

  • 非公平锁:等待时间短,但访问顺序随机


  1. 影响因素


  • 公平锁:只影响当前等待的线程,不影响新来线程

  • 非公平锁:可能会无限次让新来线程抢占锁,导致老线程永远获取不到锁


  1. 线程饥饿


  • 公平锁:旧线程有获取锁的机会,相对更公平

  • 非公平锁:可能导致线程饥饿问题


所以综上,非公平锁性能更高,但公平锁更公平。由于性能测试中通常对性能是有要求的,若非强需求,建议尽量使用非公平锁。


发布于: 刚刚阅读数: 5
用户头像

FunTester

关注

公众号:FunTester,800篇原创,欢迎关注 2020-10-20 加入

Fun·BUG挖掘机·性能征服者·头顶锅盖·Tester

评论

发布
暂无评论
可重入锁ReentrantLock在性能测试常见用法_FunTester_InfoQ写作社区