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最好使用成员变量之类的,如果使用函数传进来的局部变量很容易出现代码被冲入的情况,因为是以该对象的地址为锁标记的
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!