2.3 lock 锁
如果你曾经遭遇过线程不安全的问题,一定不会对 “锁” 这个概念不陌生。实际上绝大多数线程安全的先解决方案都离不开 “锁”。
JDK 里面就有一个接口java.util.concurrent.locks.Lock
,顾名思义,就是并发包中 “锁”,大量的线程安全问题解决方案均是依赖这个接口的实现类。就跟 synchronized 关键字一样,在性能测试实战中只要掌握基本的功能和最佳实战即可,这里再重复一下上一节的建议:如需使用 Lock 实现的功能过于复杂,建议抛开 Lock,寻找更加简单、可靠,已验证的解决方案。
在性能测试中最常用的java.util.concurrent.locks.Lock
实现类就是可重入锁:java.util.concurrent.locks.ReentrantLock
。相比synchronized
,ReentrantLock
拥有以下主要优点:
可重入性。ReentrantLock
允许已经获取锁的线程再次获取锁,相比 d 更加安全,避免发生死锁的情况。
更加灵活。d 提供多个 API 完成锁的获取和释放,让使用者拥有更多选择。
可中断性。ReentrantLock
功能中,获取锁的线程可以被主动中断,相比synchronized
无限等待,更加适合处理锁的超时场景。
更高的性能。除了提供多种获取锁的 API 以外,ReentrantLock
还提供两种锁类型:公平锁和非公平锁,帮助程序提升在加锁场景的性能。
ReentrantLock
提供了 3 中获取锁的 API,分别是:阻塞锁、可中断锁和超时锁。下面分别用代码演示如何使用。
2.3.1 阻塞锁
获取阻塞锁的方法是:java.util.concurrent.locks.ReentrantLock#lock
,没有参数。该方法会尝试获取锁。当无法获取锁时,当前线程会处于休眠状态,直到获取锁成功。
演示代码如下:
package org.funtester.performance.books.chapter02.section3;
import java.util.concurrent.locks.ReentrantLock;
/**
* 阻塞锁示例
*/
public class BlockingLockDemo {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();// 创建一个可重入锁
Thread lockTestThread = new Thread(() -> {// 创建一个线程
System.out.println(System.currentTimeMillis() + " 异步线程启动! " + Thread.currentThread().getName());// 打印日志
lock.lock();// 获取锁
System.out.println(System.currentTimeMillis() + " 获取到锁了! " + Thread.currentThread().getName());// 打印日志
lock.unlock();// 释放锁
});
lock.lock();// 获取锁
lockTestThread.start();// 启动异步线程
Thread.sleep(100);// 睡眠100毫秒
System.out.println(System.currentTimeMillis() + " 即将释放锁! " + Thread.currentThread().getName());// 打印日志
lock.unlock();// 释放锁
}
}
这个例子中,首先创建了一个异步线程,执行代码逻辑为:获取锁,打印日志,释放锁。然后在 main 线程中,先获取锁,再启动异步线程。然后main
线程休眠 100 毫秒,再释放锁。控制台输出内容如下:
1698477535368 异步线程启动! Thread-0
1698477535471 即将释放锁! main
1698477535471 获取到锁了! Thread-0
可以看到,异步线程在启动之后,等待了 100 毫秒才获取到锁,并打印日志,且这个操作也是在 main 线程释放锁之后进行的。原因是因为 main 线程先于异步线程获取到锁了,所以在 main 线程释放锁之前,异步线程只能无所事事,干等着。
阻塞锁和synchronized
解决线程安全思路和使用方法上比较相似,而且在性能测试工作中使用场景大多重合。阻塞锁在一定程度上可以替代synchronized
,特别是在编写流程式的代码中。
2.3.2 可中断锁
可中断锁的获取方法是:java.util.concurrent.locks.ReentrantLock#lockInterruptibly
,没有参数。该方式会尝试获取锁,并且是阻塞的,但当未获取到锁时,如果当前线程被设置了中断状态,则会抛出 java.lang.InterruptedException 异常。
下面是演示代码:
package org.funtester.performance.books.chapter02.section3;
import java.util.concurrent.locks.ReentrantLock;
/**
* 可中断锁示例
*/
public class InterruptiblyLockDemo {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();// 创建一个可重入锁
Thread lockTestThread = new Thread(() -> {// 创建一个线程
try {
lock.lockInterruptibly();// 获取锁
System.out.println(System.currentTimeMillis() + " 获取到锁了! " + Thread.currentThread().getName());// 打印日志
lock.unlock();// 释放锁
} catch (InterruptedException e) {
System.out.println(System.currentTimeMillis() + " 线程被中断了! " + Thread.currentThread().getName());// 打印日志
}
});
lock.lock();// 获取锁
lockTestThread.start();// 启动异步线程
lockTestThread.interrupt();// 中断异步线程
lock.unlock();// 释放锁
}
}
在这个例子中,首先创建了一个异步线程,执行代码逻辑为获取锁(可中断),打印日志,释放锁。然后让 main 线程先获取锁,然后启动异步线程,再中断异步线程。下面来就控制台输出:
1698478061924 线程被中断了! Thread-0
这里看到只有一行输出,即异步线程再获取锁时被中断了,抛出的异常被捕获。
可中断锁继承了阻塞锁的有点,提供了将线程从等待中解脱的方案,在使用上更加广泛。可中断锁可以进行线程间超时控制、防止无限等待,可以非常优雅地关闭被阻塞的线程,释放资源。可中断锁适合多线程协作的场景,要求使用者对多线程了解也更高。
2.3.3 超时锁
超时锁的获取方法有两个:java.util.concurrent.locks.ReentrantLock#tryLock()
和 java.util.concurrent.locks.ReentrantLock#tryLock(long, java.util.concurrent.TimeUnit)
,返回Boolean
值,表示获取锁是否成功。第二个 API 参数设置超时时间。这两个 API 前者可以简单理解为后者时间设置为 0,含义是尝试获取一次,返回结果。
演示代码如下:
package org.funtester.performance.books.chapter02.section3;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 超时锁示例
*/
public class TimeoutLockDemo {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();// 创建一个可重入锁
Thread lockTestThread = new Thread(() -> {// 创建一个线程
boolean b = lock.tryLock();// 第一次尝试获取锁
System.out.println(System.currentTimeMillis() + " 第一次获取锁的结果:" + b + " " + Thread.currentThread().getName());// 打印日志
try {
boolean b1 = lock.tryLock(3, TimeUnit.SECONDS);
System.out.println(System.currentTimeMillis() + " 第二次获取锁的结果:" + b1 + " " + Thread.currentThread().getName());
} catch (InterruptedException e) {
System.out.println(System.currentTimeMillis() + " 第二次获取锁中断了 " + Thread.currentThread().getName());
}
});
lock.lock();// 获取锁
lockTestThread.start();// 启动异步线程
Thread.sleep(100);// 睡眠100毫秒
lock.unlock();// 释放锁
}
}
在这个例子中,依旧先创建一个异步线程,执行的逻辑为:首先尝试获取一次并且打印结果,然后第二次尝试获取,设置超时时间 3 秒,并打印结果。main 线程依旧先获取锁,然后启动异步线程,休眠 100 ms 然后释放锁。例子中,为了简化代码,笔者并没有编写依据获取锁的结果释放锁的代码。控制台输出内容如下:
1698479430990 第一次获取锁的结果:false Thread-0
1698479431090 第二次获取锁的结果:true Thread-0
可以看到第一次获取锁失败了,原因是该锁正在被 main 线程持有。第二获取锁成功了,因为 main 线程持有锁 100 毫秒之后便释放锁。在异步线程第二次获取锁的 3 秒超时时间内,它成功了,所以获取到了锁。
在三种锁的方法中,超时锁在性能测试中使用最广泛。它提供了一种简单、可靠的控制锁等待时间的方式。相比可中断锁,超时锁对新手更加容易上手,无须掌握线程间统信、调度的知识。
2.3.4 公平锁和非公平锁
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
值,含义即是否使用公平锁。无参的构造方法默认使用的非公平锁。公平锁和非公平锁的主要区别是获取锁的方式不同。公平锁的获取是公平的,线程依次排队获取锁。谁等待的时间最长,就由谁获得锁。非公平锁获取是随机的,谁先请求谁先获得锁,不一定按照请求锁的顺序来。ReentrantLock
默认的是非公平锁,相比公平锁拥有更高性能。
2.3.5 最佳实战
在性能测试实战中, java.util.concurrent.locks.ReentrantLock
而言 ,常用最佳实战非常容易掌握。那就是使用 try-catch-finally
语法实现,演示案例如下:
boolean status = false;
try {
status = lock.tryLock(3, TimeUnit.SECONDS);
} catch (Exception e) {
// 异常处理
} finally {
if (status) lock.unlock();
}
在使用ReentrantLock
解决线程安全问题时,有几点注意事项:
- 必须主动进行锁管理。与
synchronized
不同,ReentrantLock
要求必需显示获取和释放锁,特别在释放锁时,最简单的方法就是按照最佳实战,将其放在 finally 中执行。 - 竭力避免死锁。不要混合使用不同锁;不要在一个功能中使用过多的锁和
synchronized
关键字;避免多次获取锁;使用使用lockInterruptibly()
获取锁,如果在等待锁的过程中线程被中断,需要有处理代码进行后续处理。 - 尽量使用 ReentrantLock 默认的非公平锁。
虽然java.util.concurrent.locks.ReentrantLock
叫可重入锁,但是在性能测试实践当中,不建议使用可重入功能。主要原因以下两点:
- (1)增加锁竞争,影响性能。使用不当会导致同一个线程频繁获取和释放锁,增加竞争,降低程序性能。
- (2)死锁风险。如果同步代码中多次使用锁,就需要严格释放锁流程,一旦发生异常而没有捕获处理,则会造成死锁风险。
在笔者的性能测试生涯中,没有必须使用可重入特性的场景,所以在性能测试实践中,应当尽量避免使用该特性,防止异常情况发生。
书的名字:从 Java 开始做性能测试 。
如果本书内容对你有所帮助,希望各位不吝赞赏,让我可以贴补家用。赞赏两位数可以提前阅读未公开章节。我也会尝试制作本书的视频教程,包括必要的答疑。
FunTester 原创精华