前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >可重入锁ReentrantLock在性能测试常见用法

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

作者头像
FunTester
发布2023-10-24 18:50:52
1760
发布2023-10-24 18:50:52
举报
文章被收录于专栏:FunTesterFunTester

在进行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如下:

代码语言:javascript
复制
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();  
}

控制台打印:

代码语言:javascript
复制
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如下:

代码语言:javascript
复制

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();  
}

控制台打印:

代码语言:javascript
复制
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如下:

代码语言:javascript
复制
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();  
}

控制台打印:

代码语言:javascript
复制
20:05:13 559 Thread-2 第一次获取锁的结果:false
20:05:14 563 Thread-2 第二次获取锁的结果:true
20:05:14 uptime:2 s

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

最佳实践

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

代码语言:javascript
复制
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 有一个构造方法,如下:

代码语言:javascript
复制
/**  
 * 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. 线程饥饿
  • 公平锁:旧线程有获取锁的机会,相对更公平
  • 非公平锁:可能导致线程饥饿问题

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-10-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 FunTester 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 类功能概览
  • 阻塞锁
  • 可中断锁
  • 超时锁
  • 最佳实践
  • 可重入
  • 公平锁与非公平锁
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档