java - 多线程排他锁ReentrantLock

一、一个函数防止多线程重入的方法可以使用排他锁

这里有个bank函数,和对应的调用函数

复制代码
class Bank {
    private final double[] accounts;
    private ReentrantLock bankLock = new ReentrantLock();
    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        Arrays.fill(accounts, initialBalance);
    }

    public void transfer(int from, int to, double amount) {
        bankLock.lock();
        try {
            accounts[from] -= amount;
            accounts[to] += amount;
        } finally {
            bankLock.unlock();
        }
    }

    private double getTotalBalance() {
        return Arrays.stream(accounts).sum();
    }
}
复制代码

调用的函数:

复制代码
    public static final int ACCOUNTS = 10;
    public static final double INITIAL_BALANCE = 1000;
    public static final double MAX_AMOUNT = 1000;
    public static final int DELAY = 10;

    public static void main(String[] args) {
        var bank = new Bank(ACCOUNTS, INITIAL_BALANCE);
        for (int i =0; i < ACCOUNTS; ++i) {
            int fromAccount = i;

            Runnable r = () -> {
                while (true) {
                    int toAccount = (int) (bank.size() * Math.random());
                    double amount = MAX_AMOUNT * Math.random();

                    bank.transfer(fromAccount, toAccount, amount);
                }
            };
            var t = new Thread(r);
            t.start();
        }
    }
复制代码

 

如上的transfer就能实现简单的排他锁,但是如果要确保账户的余额是否足够该如何做呢?

这里先加一个成员变量 private Condition sufficientFunds; 并在构造函数中增加实现:sufficientFunds = bankLock.newCondition();

接下去我们的transfer就可以这么写了:

复制代码
    public void transfer(int from, int to, double amount) {
        bankLock.lock();
        try {
            while (accounts[from] < amount) {
                sufficientFunds.await();
            }

            accounts[from] -= amount;
            accounts[to] += amount;

            sufficientFunds.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bankLock.unlock();
        }
    }
复制代码

在要转账之前先确保账户的钱大于等于要转出的钱,如果不够就调用await,转成功之后调用signalAll唤醒其他在await的线程,去判断账户余额是否足够

这里说明下signalAll和await之间的关系

1、Condition这里维护这一个队列,用来存储所有处于await状态的信号

2、当调用await的时候就将当前的await信号放入队列的尾中

3、当signalAll的时候,会按照先进先出的原则从队列头取await信号并唤醒它(注意:取出之后该队列就会少一个await信号)

4、因为是在lock中,所以是线程安全的;不用担心一下子把队列中所有的await信号取出来从而出现重入的情况;并且是按顺序取的,只有该线程离开了unlock之后才会再唤醒下一个await

5、如果在唤醒了一个或者几个await之后又有新的await入队也是放在队列的尾中

 

二、使用synchronized代替ReentrantLock

咱们从1中可以看出使用ReentrantLock挺繁琐的,需要新建ReentrantLock和Condition,并且还需要注意再finally的地方增加unlock防止代码出问题忘记解锁

如下是使用synchronized来实现一中的功能

    public synchronized void transfer(int from, int to, double amount) throws InterruptedException {
        while (accounts[from] < amount) {
            wait();
        }
        accounts[from] -= amount;
        accounts[to] += amount;
        notifyAll();
    }

在函数的开始增加synchronized的标记说明该函数是同步函数

通过wait() 来替换之前的sufficientFunds.await();

通过notifyAll();来替换之前的sufficientFunds.signalAll();

这里不需要调用unlock, 因为在离开transfer之后自然就解锁了

 

三、使用synchronized实现代码块的同步

有时我们不想要一下子将整个函数同步,则可以使用代码块同步的方式

格式是:

synchronized(对象) {

要同步的内容

}

 

具体可以看如下的代码

    public void transfer(int from, int to, double amount) {
        synchronized (bankLock) {
            accounts[from] -= amount;
            accounts[to] += amount;
        }
    }

这里使用了一个bankLock的成员变量为同步的标志(注意:如果另外的函数也用bankLock为同步的标志则这两个代码块也是处于同步的状态,这里需要小心)

并且bankLock最好使用成员变量之类的,如果使用函数传进来的局部变量很容易出现代码被冲入的情况,因为是以该对象的地址为锁标记的

 

posted @   LCAC  阅读(156)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
点击右上角即可分享
微信分享提示