FunTester 【连载 08】lock 锁

FunTester · 2024年12月31日 · 972 次阅读

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 开始做性能测试

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

FunTester 原创精华

【连载】从 Java 开始性能测试

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