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

FunTester · 2023年10月24日 · 1938 次阅读

在进行 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 第一次获取锁的结果:false
20:05:14 563 Thread-2 第二次获取锁的结果:true
20: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. 获取锁的方式不同
  2. 公平锁:线程依次排队获取锁,效率较低
  3. 非公平锁:随机获取锁,效率较高
  4. 性能不同
  5. 公平锁:一次性唤醒队列中等待时间最久的线程,Context Switching 次数高,性能较低
  6. 非公平锁:随机唤醒线程,Context Switching 次数低,性能较高
  7. 锁等待时间
  8. 公平锁:等待时间长,但访问顺序按队列顺序
  9. 非公平锁:等待时间短,但访问顺序随机
  10. 影响因素
  11. 公平锁:只影响当前等待的线程,不影响新来线程
  12. 非公平锁:可能会无限次让新来线程抢占锁,导致老线程永远获取不到锁
  13. 线程饥饿
  14. 公平锁:旧线程有获取锁的机会,相对更公平
  15. 非公平锁:可能导致线程饥饿问题

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

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册