• Java进阶篇--公平锁 & 非公平锁


    目录

    ReentrantLock的介绍

    重入性的实现原理

    代码示例:

    公平锁与非公平锁

    代码示例:


    ReentrantLock的介绍

    ReentrantLock是Java中实现Lock接口的一种重入锁(Reentrant Lock)实现类。它提供了与synchronized关键字相似的功能(在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入),但比synchronized更加灵活和强大。ReentrantLock可以用于实现多线程之间的互斥访问和同步操作。

    下面是ReentrantLock主要的特点和用法介绍:

    1、可重入性:
    ReentrantLock允许同一个线程多次获取同一把锁,而不会发生死锁。这种特性叫做可重入性,也就是可重入锁。当一个线程已经持有锁时,再次获取锁时会增加锁的计数器,每次释放锁时计数器减一,只有当计数器为0时锁才会完全释放。

    2、获取锁的方式:
    通过调用ReentrantLock的lock()方法可以获取锁,如果锁已被其他线程获取,则当前线程会被阻塞,直到获取到锁为止。另外,ReentrantLock还提供了tryLock()方法,该方法尝试立即获取锁,如果锁未被其他线程获取,则返回true,否则返回false。tryLock()方法也可以设置超时时间,指定在一段时间内尝试获取锁。

    3、线程阻塞与唤醒:
    与synchronized不同,ReentrantLock提供了显示的线程挂起和恢复的能力。通过调用lock()方法后,如果锁已被其他线程占用,则当前线程会进入休眠状态,直到获取锁成功。当不再需要锁时,需要调用unlock()方法显式地释放锁,以唤醒其他等待线程。

    4、锁的公平性:
    ReentrantLock支持公平锁和非公平锁。可以在创建ReentrantLock对象时指定构造函数参数来设置锁的公平性。默认情况下,ReentrantLock是非公平锁。

    5、条件变量:
    与ReentrantLock配套使用的还有条件变量(Condition),可以通过ReentrantLock的newCondition()方法创建一个特定的条件变量。条件变量可以使线程在某个条件满足时等待,或者唤醒等待在该条件上的线程。

    总结:
    ReentrantLock是Java提供的一种可重入的高级锁实现,相比synchronized关键字更加灵活,并提供了更多功能,如可控的锁公平性、显示的线程挂起和恢复、超时锁等等。在多线程编程中,使用ReentrantLock可以更好地控制资源的访问和线程的同步。。

    那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:1. 重入性的实现原理;2. 公平锁和非公平锁。

    重入性的实现原理

    ReentrantLock支持重入性的实现是通过在锁对象上维护一个计数器来实现的。

    在Java中,每个线程都拥有自己的线程栈(Thread Stack)。如果一个方法被调用,那么就会在线程栈上分配一段空间,这段空间被称为栈帧(Stack Frame)。当一个方法被另一个方法调用时,就会在栈上分配另一个栈帧,这就形成了方法调用链。

    在ReentrantLock中,每个线程都有一个变量叫做"holdCount",表示该线程目前获取锁的次数。当一个线程第一次获取锁时,holdCount的值为1;如果再次获取锁,则holdCount递增,以此类推。当这个线程释放锁时,holdCount递减。只有当holdCount的值为0时,锁才真正释放。

    也就是说,ReentrantLock对于每个线程来说,维护了一个独立的计数器。当一个线程试图获取锁时,首先判断当前线程是否已经获取了该锁,如果已经获取,则直接返回true;如果未获取,则需要尝试获取锁。当该线程成功获取锁时,holdCount递增,表示当前线程又多获取了一次该锁。当该线程释放锁时,holdCount递减,表示当前线程又释放了一次该锁。只有当holdCount的值为0时,锁才会真正释放。

    总结一下:在ReentrantLock中,通过为每个线程维护一个计数器(即holdCount)来实现重入性。当一个线程多次获取锁时,计数器递增;每次释放锁时,计数器相应地递减。只有当计数器为0时,锁才完全释放,其他线程才能获取该锁。这种机制可以避免死锁和线程饥饿等问题,是一种非常有效的并发控制手段。

    代码示例:

    1. import java.util.concurrent.locks.ReentrantLock;
    2. public class Main {
    3. private ReentrantLock lock = new ReentrantLock();
    4. private double balance;
    5. public Main(double initialBalance) {
    6. balance = initialBalance;
    7. }
    8. // 存款
    9. public void deposit(double amount) {
    10. lock.lock(); // 获取锁
    11. try {
    12. balance += amount; // 更新余额
    13. System.out.println("存款成功,当前余额为:" + balance);
    14. } finally {
    15. lock.unlock(); // 释放锁
    16. }
    17. }
    18. // 取款
    19. public void withdraw(double amount) {
    20. lock.lock(); // 获取锁
    21. try {
    22. if (balance >= amount) {
    23. balance -= amount; // 更新余额
    24. System.out.println("取款成功,当前余额为:" + balance);
    25. } else {
    26. System.out.println("余额不足");
    27. }
    28. } finally {
    29. lock.unlock(); // 释放锁
    30. }
    31. }
    32. public static void main(String[] args) {
    33. Main account = new Main(1000.0);
    34. // 创建两个线程模拟两个用户进行存款和取款操作
    35. Thread depositThread = new Thread(() -> {
    36. for (int i = 0; i < 5; i++) {
    37. account.deposit(100.0);
    38. }
    39. });
    40. Thread withdrawThread = new Thread(() -> {
    41. for (int i = 0; i < 5; i++) {
    42. account.withdraw(200.0);
    43. }
    44. });
    45. depositThread.start();
    46. withdrawThread.start();
    47. }
    48. }

    在这个例子中,我们模拟了一个银行账户,通过使用ReentrantLock实现对余额的线程安全操作。在存款和取款方法中,我们都先获取锁,执行相应的操作,然后释放锁。

    在主函数中,我们创建了两个线程,一个用于模拟存款操作,另一个用于模拟取款操作。每个线程都执行5次操作。通过使用ReentrantLock,我们确保了每次存款或取款操作是原子的,不会发生并发访问的问题。同时,由于ReentrantLock支持重入性,所以同一个线程可以多次获取锁,这也是允许的。

    公平锁与非公平锁

    公平锁和非公平锁是ReentrantLock(可重入锁)中的两种获取锁的策略。

    • 公平锁在多线程竞争下,按照线程的请求顺序来获取锁,保证了请求资源时间上的绝对顺序。当一个线程释放锁后,同步队列中的第一个等待线程会获取到锁,其他线程按照先来后到的顺序依次获取锁。公平锁可以避免线程的饥饿现象,但可能因为频繁的上下文切换而降低系统的吞吐量。
    • 非公平锁在多线程竞争下,不保证线程获取锁的顺序。当一个线程释放锁后,新的线程有机会直接获取到锁,即使其他线程在等待获取锁。非公平锁的实现较为简单高效,可以减少上下文切换的次数,提高系统的吞吐量。但是,某些线程可能会长时间等待,造成其他线程一直获取锁的情况。

    ReentrantLock默认是非公平锁,因为在大多数情况下,非公平锁性能更好。如果需要创建公平锁,可以通过构造函数参数传入true,创建一个公平锁的实例:

    1. ReentrantLock fairLock = new ReentrantLock(true); // 创建公平锁
    2. //注意:使用ReentrantLock类创建非公平锁时,不需要传入参数或者传入false

    需要注意的是,公平锁会增加一定的上下文切换次数,适用于对线程获取锁的顺序有严格要求的场景,而非公平锁适用于追求更高吞吐量的场景。

    总结:

    • 公平锁按照线程申请锁的顺序获取锁,避免线程饥饿,但可能会引起较大的开销。
    • 非公平锁没有严格的获取顺序,可能会导致后申请的线程先获取锁,但能够获得更高的吞吐量。

    在实际应用中,选择公平锁还是非公平锁取决于具体的场景和需求。一般来说,在线程竞争激烈的情况下,非公平锁可能更适合,可以提升系统性能。

    代码示例:

    1. import java.util.concurrent.locks.ReentrantLock;
    2. public class Main {
    3. private static ReentrantLock fairLock = new ReentrantLock(true); // 创建公平锁
    4. private static ReentrantLock nonFairLock = new ReentrantLock(false); // 创建非公平锁
    5. public static void main(String[] args) {
    6. // 公平锁示例
    7. Thread fairThread1 = new Thread(() -> {
    8. fairLock.lock(); // 获取公平锁
    9. try {
    10. System.out.println("公平锁 - 线程1获取到锁");
    11. Thread.sleep(1000);
    12. } catch (InterruptedException e) {
    13. e.printStackTrace();
    14. } finally {
    15. fairLock.unlock(); // 释放公平锁
    16. System.out.println("公平锁 - 线程1 释放公平锁");
    17. }
    18. });
    19. Thread fairThread2 = new Thread(() -> {
    20. fairLock.lock(); // 获取公平锁
    21. try {
    22. System.out.println("公平锁 - 线程2获取到锁");
    23. Thread.sleep(1000);
    24. } catch (InterruptedException e) {
    25. e.printStackTrace();
    26. } finally {
    27. fairLock.unlock(); // 释放公平锁
    28. System.out.println("公平锁 - 线程2 释放公平锁");
    29. }
    30. });
    31. // 非公平锁示例
    32. Thread nonFairThread1 = new Thread(() -> {
    33. nonFairLock.lock(); // 获取非公平锁
    34. try {
    35. System.out.println("非公平锁 - 线程1获取到锁");
    36. Thread.sleep(1000);
    37. } catch (InterruptedException e) {
    38. e.printStackTrace();
    39. } finally {
    40. nonFairLock.unlock(); // 释放非公平锁
    41. System.out.println("非公平锁 - 线程1 释放非公平锁");
    42. }
    43. });
    44. Thread nonFairThread2 = new Thread(() -> {
    45. nonFairLock.lock(); // 获取非公平锁
    46. try {
    47. System.out.println("非公平锁 - 线程2获取到锁");
    48. Thread.sleep(1000);
    49. } catch (InterruptedException e) {
    50. e.printStackTrace();
    51. } finally {
    52. nonFairLock.unlock(); // 释放非公平锁
    53. System.out.println("非公平锁 - 线程2 释放非公平锁");
    54. }
    55. });
    56. fairThread1.start();
    57. fairThread2.start();
    58. nonFairThread1.start();
    59. nonFairThread2.start();
    60. }
    61. }

    在上述示例中,我们创建了一个公平锁(fairLock)和一个非公平锁(nonFairLock),然后创建了两个线程来演示这两种锁的行为。

    • 对于公平锁,我们创建了两个线程(fairThread1和fairThread2),它们按照先来后到的顺序获取锁。由于是公平锁,fairThread1首先获取到锁,然后才轮到fairThread2获取锁。
    • 对于非公平锁,我们创建了两个线程(nonFairThread1和nonFairThread2),它们竞争获取锁。由于是非公平锁,在某些情况下,刚释放锁的线程有机会再次获取锁,因此可能会导致其他线程长时间等待。

    通过运行上述代码,我们可以观察到公平锁和非公平锁的不同行为。请注意,输出结果可能因操作系统调度而有所差异,但公平锁会按照线程的请求顺序获取锁,而非公平锁则不保证顺序。

  • 相关阅读:
    检查 Oracle 版本的 7 种方法
    linux安装mysql
    如何与同事相处
    【无标题】
    【产品经理修炼之道】- 从0到1搭建风筝比赛小程序
    分支限界法和回溯发法解决无优先级运算问题
    关于二阶低通滤波的C代码及入门测试
    Qt-制作一个简单的计算器-实现四则运算-将结果以对话框的形式弹出来
    使用Python将MySQL查询结果导出到Excel(xlsxwriter)
    【python技巧】替换文件中的某几行
  • 原文地址:https://blog.csdn.net/m0_74293254/article/details/133838921