Java内置锁:深度解析ReentrantLock并发类

发布时间:2024年01月15日

Java内置锁:深度解析ReentrantLock并发类 - 程序员古德

ReentrantLock是Java提供的强大且灵活的可重入锁,支持公平和非公平特性。在业务场景中,如在线售票系统,可用它解决多线程并发访问共享资源问题。通过互斥访问确保数据一致性和避免超卖,只有获锁的线程可执行出票操作。ReentrantLock的可重入性避免死锁,增强了系统的稳定性。

定义

Java内置锁:深度解析ReentrantLock并发类 - 程序员古德

ReentrantLock是Java提供的一个可重入锁,并且支持在构造函数中指定公平或者非公平的特性,它的功能比synchronized关键字更为强大和灵活,在实际的业务场景中,经常用到ReentrantLock来解决多线程并发访问共享资源的问题。

假设有一个在线售票系统,这个系统允许多个用户同时购买不同场次的电影票,为了保证数据的一致性和避免超卖的情况,每次用户购票时,系统都需要先检查剩余的票数是否足够,如果足够则进行出票操作,并相应地减少库存。

在这里,可以使用ReentrantLock来实现对共享资源(即剩余的票数)的互斥访问,具体的做法是,在出票操作开始前,先尝试获取锁,如果获取成功,则执行出票操作,并在操作完成后释放锁;如果获取失败(说明有其他线程正在执行出票操作),则当前线程需要等待,直到获取到锁为止*(注意:这场景并不适合在分布式情况下,分布式情况下需要使用分布式锁来实现)*。

这样做的好处是,可以确保同一时间只有一个线程在执行出票操作,从而避免了多线程并发访问导致的数据不一致和超卖问题,同时,由于ReentrantLock支持可重入性,如果一个线程已经获取了锁,那么它可以再次获取同一个锁而不会发生死锁,这个特性非常有用。

代码案例

Java内置锁:深度解析ReentrantLock并发类 - 程序员古德

下面是使用ReentrantLock实现的简单的“在线售票系统”Java代码示例,在这个示例中,TicketSeller类封装了售票的逻辑,使用一个ReentrantLock来保证线程安全,ClientThread类模拟了多个客户端同时购票的场景,如下代码:

import java.util.concurrent.locks.ReentrantLock;  
  
// 售票系统类  
public class TicketSeller {  
    // 假设总共有100张票  
    private int tickets = 100;  
    // 创建一个可重入锁  
    private final ReentrantLock lock = new ReentrantLock();  
  
    // 售票方法  
    public void sellTicket() {  
        lock.lock(); // 加锁  
        try {  
            // 检查是否有票可售  
            if (tickets > 0) {  
                System.out.println(Thread.currentThread().getName() + " 售出1张票,剩余票数:" + (--tickets));  
            } else {  
                System.out.println("票已售完!");  
            }  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
}  
  
// 客户端线程类  
public class ClientThread extends Thread {  
    private TicketSeller seller;  
  
    public ClientThread(String name, TicketSeller seller) {  
        super(name);  
        this.seller = seller;  
    }  
  
    @Override  
    public void run() {  
        // 模拟客户端连续购票  
        for (int i = 0; i < 5; i++) {  
            seller.sellTicket();  
            try {  
                // 每次购票后休眠一段时间,模拟真实场景中的时间间隔  
                Thread.sleep(100);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  
  
// 主类,用于启动售票系统和客户端线程  
public class TicketSystemDemo {  
    public static void main(String[] args) {  
        // 创建一个售票系统实例  
        TicketSeller seller = new TicketSeller();  
        // 创建多个客户端线程并启动  
        for (int i = 1; i <= 5; i++) {  
            new ClientThread("客户端" + i, seller).start();  
        }  
    }  
}

在上面代码中,先定义了一个TicketSeller类,它包含了售票的业务逻辑,方法返回的是正等待获取此锁的线程数,且使用了ReentrantLock来保证在多线程环境下,对共享资源(剩余的票数)的访问是线程安全的。

API列表

Java内置锁:深度解析ReentrantLock并发类 - 程序员古德

API说明

Modifier and TypeMethodDescription
intgetHoldCount()返回当前线程对此锁的持有计数,也就是说当前线程已经获取了这个锁多少次。
protected ThreadgetOwner()返回当前持有该锁的线程Thread对象
protected CollectiongetQueuedThreads()返回正等待获取此锁的线程集合。
intgetQueueLength()返回正等待获取此锁的线程数。
protected CollectiongetWaitingThreads(Condition condition)返回与此锁关联的给定条件上等待的线程的集合。
intgetWaitQueueLength(Condition condition)返回与此锁关联的给定条件上等待的线程数。
booleanhasQueuedThread(Thread thread)返回给定线程是否正在等待获取此锁。
booleanhasQueuedThreads()返回是否有线程正在等待获取此锁。
booleanhasWaiters(Condition condition)判断是否有线程正在等待获取该锁。
booleanisFair()返回是否是公平锁。
booleanisHeldByCurrentThread()返回线程自己当前是否持有这个锁。
booleanisLocked()返回此锁是否被任何线程持有。
voidlock()Acquires the lock.
voidlockInterruptibly()Acquires the lock unless the current thread is interrupted.
ConditionnewCondition()Returns a Condition instance for use with this Lock instance.
StringtoString()Returns a string identifying this lock, as well as its lock state.
booleantryLock()Acquires the lock only if it is not held by another thread at the time of invocation.
booleantryLock(long timeout, TimeUnit unit)Acquires the lock if it is not held by another thread within the given waiting time and the current thread has not been interrupted.
voidunlock()Attempts to release this lock.

核心API

getHoldCount()

getHoldCount()方法返回当前线程对此锁的持有计数,也就是当前线程重复获取此锁的次数。

假设一个场景:在一个多线程环境中,有一个资源(比如一个文件)需要被多个方法共享访问,但为了保证数据的一致性和完整性,需要在每个方法内部对此资源进行加锁,比如:线程A在执行某个方法时首次获取了锁,并在方法内部调用了另一个也需要访问该资源的方法,由于锁是可重入的,线程A可以再次获取同一个锁而不会阻塞,这时,如果想知道线程A到底获取了多少次这个锁,就可以使用getHoldCount()方法。

下面代码案例,展示了如何使用ReentrantLock和getHoldCount()方法,如下代码:

import java.util.concurrent.locks.ReentrantLock;  
  
public class ReentrantLockDemo {  
    // 创建一个可重入锁实例  
    private final ReentrantLock lock = new ReentrantLock();  
    // 计数器资源  
    private int counter = 0;  
  
    // 增加计数器的方法  
    public void increment() {  
        lock.lock(); // 加锁  
        try {  
            counter++;  
            System.out.println(Thread.currentThread().getName() + " incremented counter to " + counter);  
            // 打印当前线程对此锁的持有计数  
            System.out.println(Thread.currentThread().getName() + " hold count: " + lock.getHoldCount());  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
  
    // 客户端调用示例  
    public static void main(String[] args) {  
        ReentrantLockDemo demo = new ReentrantLockDemo();  
        // 创建并启动两个线程来访问共享资源  
        Thread thread1 = new Thread(() -> {  
            demo.increment();  
            demo.increment(); // 线程重入锁  
        }, "Thread-1");  
  
        Thread thread2 = new Thread(() -> {  
            demo.increment();  
        }, "Thread-2");  
  
        // 启动线程  
        thread1.start();  
        thread2.start();  
    }  
}

在上面代码汇中,有一个ReentrantLockDemo类,它包含一个ReentrantLock实例和一个计数器,increment()方法用于增加计数器的值,并且在每次调用时都会打印当前线程对锁的持有计数,在main()方法中,创建了两个线程,它们都会调用increment()方法,第一个线程会调用两次increment(),因此它会重入锁一次,第二个线程只调用一次increment()。

如下代码输出结果:

Thread-1 incremented counter to 1  
Thread-1 hold count: 1  
Thread-1 incremented counter to 2  
Thread-1 hold count: 2  
Thread-2 incremented counter to 3  
Thread-2 hold count: 1

hasQueuedThreads()

hasQueuedThreads()方法用于检查是否有任何线程正在等待获取此锁。

假设一个场景:当前有一个多线程应用,其中有一个共享资源需要被多个线程安全地访问,为了保护这个资源,可以使用ReentrantLock来确保每次只有一个线程能够访问它,但是,随着项目的不断迭代,应用规模变得越来越复杂,锁的竞争越来越频繁,因此希望在程序运行时获得是否有线程正在等待获取这个锁,以便做出优化和调整,这时,可以使用hasQueuedThreads()实现。

比如,在定时任务中定时通过调用hasQueuedThreads()来检查锁的等待队列,如果该方法返回true,说明有线程正在等待获取锁,这可能意味着共享资源的访问竞争比较激烈,可能需要考虑优化资源的使用方式,或者增加资源的数量来减少等待,反之,如果返回false,则说明当前没有线程在等待,资源的使用可能比较平稳,如下示例代码:

import java.util.concurrent.locks.ReentrantLock;  
  
public class ResourcePool {  
    // 模拟资源池  
    private final Object[] resources;  
    // 用于同步访问的可重入锁  
    private final ReentrantLock lock = new ReentrantLock();  
  
    public ResourcePool(int size) {  
        resources = new Object[size];  
        for (int i = 0; i < size; i++) {  
            resources[i] = new Object();  
        }  
    }  
  
    // 获取资源的方法  
    public Object getResource() {  
        lock.lock(); // 加锁  
        try {  
            // 模拟资源获取时的延迟  
            try {  
                Thread.sleep(100);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            // 返回资源(这里仅作示例,实际上应该是返回资源池中的一个资源)  
            return new Object();  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
  
    // 检查是否有线程在等待获取锁,并输出信息  
    public void checkIfThreadsWaiting() {  
        if (lock.hasQueuedThreads()) {  
            System.out.println("有线程正在等待获取锁!");  
        } else {  
            System.out.println("当前没有线程在等待获取锁。");  
        }  
    }  
  
    // 客户端调用示例  
    public static void main(String[] args) {  
        ResourcePool pool = new ResourcePool(5);  
  
        // 创建并启动多个线程来访问资源池  
        for (int i = 0; i < 10; i++) {  
            new Thread(() -> {  
                pool.getResource(); // 请求资源  
                System.out.println(Thread.currentThread().getName() + " 获取到资源。");  
            }).start();  
        }  
  
        // 让主线程稍微等待一下,以便其他线程有机会开始等待锁  
        try {  
            Thread.sleep(200);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
  
        // 检查是否有线程在等待获取锁  
        pool.checkIfThreadsWaiting();  
    }  
}

hasWaiters()

ReentrantLock 类中的 hasWaiters() 方法主要用于检查是否有线程正在等待获取此锁。

举一个场景:当前正在开发一个多线程的在线书店应用,其中有一个关键的资源是库存量。每当有顾客下单购买书籍时,应用需要更新库存量,为了防止多个线程同时更新库存导致数据不一致,使用了 ReentrantLock 来确保一次只有一个线程可以更新库存。

但是在某些情况下,可能想知道是否有线程正在等待更新库存,例如,如果系统管理员想要监控应用的健康状况,了解是否有过多的线程在等待锁这是一个非常重要的监控指标,过多的等待线程可能意味着系统遇到了瓶颈,需要进行优化。

这时,hasWaiters() 方法就派上用场了,以通过查询这个方法来了解当前是否有线程在等待获取库存锁,如果 hasWaiters() 返回 true,那么就知道有线程在等待,可能需要进一步调查原因,比如检查是否有死锁情况发生,或者考虑增加资源以减少等待时间。如果返回 false,则说明目前没有线程在等待,系统可能运行得相对顺畅。

isLocked()

isLocked() 方法用于检查此锁是否被任何线程持有。

当前正在开发一个多线程的库存管理系统,在这个系统中,有一个共享资源,比如一个仓库,多个线程可能同时尝试访问或修改这个仓库的状态,在这种情况下,isLocked() 方法就非常有用了,可以调用这个方法来检查锁是否被持有,如果 isLocked() 返回 true,就知道有线程正在修改仓库,这时可以选择等待、尝试获取锁或者执行其他适当的操作,如果返回 false,就知道当前没有线程持有该锁,这时可以安全地执行操作。

公平和非公平

ReentrantLock 是一个可重入互斥锁,它有两个模式:公平锁和非公平锁,以下是两者之间的主要区别:

公平锁(Fair Lock)

  1. 顺序性:公平锁会按照线程请求锁的顺序来分配锁,也就是说,如果线程A首先请求了锁,然后线程B也请求了锁,那么只有当线程A释放锁后,线程B才能获得锁,这种顺序性的保证确保了所有等待锁的线程都有机会按照它们请求锁的顺序来获得锁。
  2. 性能:由于公平锁需要维护一个队列来记录等待锁的线程,并且在每次锁释放时都需要检查这个队列,所以它的性能相对于非公平锁来说会稍微差一些。
  3. 避免饥饿:公平锁能够避免线程饥饿问题,线程饥饿是指一个或多个线程因为无法获取所需的资源(在这种情况下是锁)而无法继续执行的情况,由于公平锁按照请求顺序分配锁,因此它确保了每个等待的线程最终都会获得锁。

非公平锁(Non-Fair Lock)

  1. 无序性:非公平锁不会按照线程请求锁的顺序来分配锁,当锁被释放时,任何等待的线程都有可能获得锁,这取决于哪个线程能够首先抢到锁,这种无序性可能导致某些线程长时间得不到执行,从而引发线程饥饿问题。
  2. 性能:由于非公平锁不需要维护一个等待队列,并且在锁释放时可以立即分配给任何一个等待的线程,所以它的性能通常会比公平锁好一些。
  3. 不确定性:非公平锁的行为更加不确定,因为无法预测哪个等待的线程会获得锁,这种不确定性可能使得程序更加难以调试和测试。

总结:公平锁和非公平锁之间的选择取决于具体的应用场景和需求,如果希望确保所有线程都能按照请求锁的顺序获得锁,并且能够避免线程饥饿问题,那么公平锁是一个不错的选择,然而,如果,更关心性能,并且能够容忍一定程度的不确定性和可能的线程饥饿问题,那么非公平锁可能是一个更好的选择。

非公平实现

import java.util.concurrent.locks.ReentrantLock;  
  
public class NonFairLockExample {  
  
    // 创建一个非公平的ReentrantLock  
    private final ReentrantLock lock = new ReentrantLock(false); // false表示非公平锁  
  
    public void doSomething() {  
        lock.lock(); // 获取锁  
        try {  
            // 模拟一些工作  
            System.out.println(Thread.currentThread().getName() + " is doing something.");  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
  
    public static void main(String[] args) {  
        NonFairLockExample example = new NonFairLockExample();  
  
        // 创建并启动多个线程  
        for (int i = 0; i < 5; i++) {  
            new Thread(() -> example.doSomething()).start();  
        }  
    }  
}

NonFairLockExample 类中有一个 ReentrantLock 的实例 lock,在构造它时传递了 false 作为参数,表示这是一个非公平锁。在 main 方法中,创建了 NonFairLockExample 的实例,并启动了5个线程来调用 doSomething 方法,由于这是一个非公平锁,所以线程获取锁的顺序是不确定的,每次运行这个程序时,输出中线程的顺序都可能会不同,如下输出示例(顺序可能会变化):

Thread-0 is doing something.  
Thread-3 is doing something.  
Thread-2 is doing something.  
Thread-4 is doing something.  
Thread-1 is doing something.

公平实现

import java.util.concurrent.locks.ReentrantLock;  
  
public class FairLockExample {  
  
    // 创建一个公平的ReentrantLock  
    private final ReentrantLock lock = new ReentrantLock(true); // true表示公平锁  
  
    public void doSomething() {  
        lock.lock(); // 获取锁  
        try {  
            // 模拟一些工作  
            System.out.println(Thread.currentThread().getName() + " is doing something.");  
            Thread.sleep(1000); // 让线程暂停一段时间,以便观察锁的行为  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
  
    public static void main(String[] args) {  
        FairLockExample example = new FairLockExample();  
  
        // 创建并启动多个线程  
        for (int i = 0; i < 5; i++) {  
            final int threadNum = i;  
            new Thread(() -> {  
                // 为了使输出更有序,让线程在开始之前稍微等待一下  
                try {  
                    Thread.sleep(100 * threadNum);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                example.doSomething();  
            }, "Thread-" + i).start();  
        }  
    }  
}

FairLockExample 类中有一个 ReentrantLock 的实例 lock,在构造它时传递了 true 作为参数,表示这是一个公平,在 main 方法中,创建了 FairLockExample 的实例,并启动了5个线程来调用 doSomething 方法。

当运行这个程序时,由于使用的是公平锁,因此线程将大致按照它们尝试获取锁的顺序来执行。如下输出。然而,即使使用了公平锁,也不能保证线程执行的绝对顺序,因为线程调度仍然受到操作系统和JVM的影响。

输出示例(大致按照线程启动顺序):

Thread-0 is doing something.  
Thread-1 is doing something.  
Thread-2 is doing something.  
Thread-3 is doing something.  
Thread-4 is doing something.


关注我,每天学习互联网编程技术 - 程序员古德

文章来源:https://blog.csdn.net/qusikao/article/details/135604332
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。