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

2023-10-24 18:50:52 浏览数 (2)

在进行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. 线程饥饿
  • 公平锁:旧线程有获取锁的机会,相对更公平
  • 非公平锁:可能导致线程饥饿问题

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

0 人点赞