写点什么

【连载 08】lock 锁

作者:FunTester
  • 2024-12-31
    河北
  • 本文字数:4519 字

    阅读完需:约 15 分钟

2.3 lock 锁

如果你曾经遭遇过线程不安全的问题,一定不会对“锁”这个概念不陌生。实际上绝大多数线程安全的先解决方案都离不开“锁”。


JDK 里面就有一个接口java.util.concurrent.locks.Lock,顾名思义,就是并发包中“锁”,大量的线程安全问题解决方案均是依赖这个接口的实现类。就跟 synchronized 关键字一样,在性能测试实战中只要掌握基本的功能和最佳实战即可,这里再重复一下上一节的建议:如需使用 Lock 实现的功能过于复杂,建议抛开 Lock,寻找更加简单、可靠,已验证的解决方案。


在性能测试中最常用的java.util.concurrent.locks.Lock实现类就是可重入锁:java.util.concurrent.locks.ReentrantLock。相比synchronizedReentrantLock拥有以下主要优点:


可重入性。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 开始做性能测试


如果本书内容对你有所帮助,希望各位不吝赞赏,让我可以贴补家用。赞赏两位数可以提前阅读未公开章节。我也会尝试制作本书的视频教程,包括必要的答疑。

发布于: 刚刚阅读数: 4
用户头像

FunTester

关注

公众号:FunTester,800篇原创,欢迎关注 2020-10-20 加入

Fun·BUG挖掘机·性能征服者·头顶锅盖·Tester

评论

发布
暂无评论
【连载 08】lock锁_FunTester_InfoQ写作社区