在进行 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();
}
复制代码
尽量使用超时锁
尽可能少占用锁
尽量低频使用
可重入
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 值,含义既是是否使用公平锁。无参的构造方法默认使用的非公平锁。公平锁和非公平锁的主要区别是获取锁的方式不同。公平锁的获取是公平的,线程依次排队获取锁。谁等待的时间最长,就由谁获得锁。非公平锁获取是随机的,谁先请求谁先获得锁,不一定按照请求锁的顺序来。
具体区别如下:
获取锁的方式不同
公平锁:线程依次排队获取锁,效率较低
非公平锁:随机获取锁,效率较高
性能不同
公平锁:一次性唤醒队列中等待时间最久的线程,Context Switching 次数高,性能较低
非公平锁:随机唤醒线程,Context Switching 次数低,性能较高
锁等待时间
公平锁:等待时间长,但访问顺序按队列顺序
非公平锁:等待时间短,但访问顺序随机
影响因素
线程饥饿
公平锁:旧线程有获取锁的机会,相对更公平
非公平锁:可能导致线程饥饿问题
所以综上,非公平锁性能更高,但公平锁更公平。由于性能测试中通常对性能是有要求的,若非强需求,建议尽量使用非公平锁。
评论