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时,锁才完全释放,其他线程才能获取该锁。这种机制可以避免死锁和线程饥饿等问题,是一种非常有效的并发控制手段。

代码示例:

import java.util.concurrent.locks.ReentrantLock;

public class Main {
    private ReentrantLock lock = new ReentrantLock();
    private double balance;

    public Main(double initialBalance) {
        balance = initialBalance;
    }

    // 存款
    public void deposit(double amount) {
        lock.lock(); // 获取锁
        try {
            balance += amount; // 更新余额
            System.out.println("存款成功,当前余额为:" + balance);
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    // 取款
    public void withdraw(double amount) {
        lock.lock(); // 获取锁
        try {
            if (balance >= amount) {
                balance -= amount; // 更新余额
                System.out.println("取款成功,当前余额为:" + balance);
            } else {
                System.out.println("余额不足");
            }
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public static void main(String[] args) {
        Main account = new Main(1000.0);

        // 创建两个线程模拟两个用户进行存款和取款操作
        Thread depositThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                account.deposit(100.0);
            }
        });

        Thread withdrawThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                account.withdraw(200.0);
            }
        });

        depositThread.start();
        withdrawThread.start();
    }
}

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

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

公平锁与非公平锁

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

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

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

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

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

总结:

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

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

代码示例:

import java.util.concurrent.locks.ReentrantLock;

public class Main {
    private static ReentrantLock fairLock = new ReentrantLock(true);  // 创建公平锁
    private static ReentrantLock nonFairLock = new ReentrantLock(false);  // 创建非公平锁

    public static void main(String[] args) {
        // 公平锁示例
        Thread fairThread1 = new Thread(() -> {
            fairLock.lock();  // 获取公平锁
            try {
                System.out.println("公平锁 - 线程1获取到锁");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                fairLock.unlock();  // 释放公平锁
                System.out.println("公平锁 - 线程1 释放公平锁");
            }
        });

        Thread fairThread2 = new Thread(() -> {
            fairLock.lock();  // 获取公平锁
            try {
                System.out.println("公平锁 - 线程2获取到锁");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                fairLock.unlock();  // 释放公平锁
                System.out.println("公平锁 - 线程2 释放公平锁");
            }
        });

        // 非公平锁示例
        Thread nonFairThread1 = new Thread(() -> {
            nonFairLock.lock();  // 获取非公平锁
            try {
                System.out.println("非公平锁 - 线程1获取到锁");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                nonFairLock.unlock();  // 释放非公平锁
                System.out.println("非公平锁 - 线程1 释放非公平锁");
            }
        });

        Thread nonFairThread2 = new Thread(() -> {
            nonFairLock.lock();  // 获取非公平锁
            try {
                System.out.println("非公平锁 - 线程2获取到锁");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                nonFairLock.unlock();  // 释放非公平锁
                System.out.println("非公平锁 - 线程2 释放非公平锁");
            }
        });

        fairThread1.start();
        fairThread2.start();
        nonFairThread1.start();
        nonFairThread2.start();
    }
}

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

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

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

你可能感兴趣的:(Java进阶篇,java,开发语言)