千家信息网

高并发场景下怎么优化加锁方式

发表于:2024-12-03 作者:千家信息网编辑
千家信息网最后更新 2024年12月03日,本篇内容介绍了"高并发场景下怎么优化加锁方式"的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!例如在我们
千家信息网最后更新 2024年12月03日高并发场景下怎么优化加锁方式

本篇内容介绍了"高并发场景下怎么优化加锁方式"的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

例如在我们完成转账操作的过程中,我们一次性申请账户A和账户B,两个账户都申请成功后,再执行转账的操作。其中,在我们实现的转账方法中,使用了死循环来循环获取资源,直到同时获取到账户A和账户B为止,核心代码如下所示。

//一次申请转出账户和转入账户,直到成功 while(!requester.applyResources(this, target)){     //循环体为空     ; }

如果ResourcesRequester类的applyResources()方法执行的时间非常短,并且程序并发带来的冲突不大,程序循环几次到几十次就可以同时获取到转出账户和转入账户,这种方案就是可行的。

但是,如果ResourcesRequester类的applyResources()方法执行的时间比较长,或者说,程序并发带来的冲突比较大,此时,可能需要循环成千上万次才能同时获取到转出账户和转入账户。这样就太消耗CPU资源了,此时,这种方案就是不可行的了。

那么,有没有什么方式对这种方案进行优化呢?

问题分析

既然使用死循环一直获取资源这种方案存在问题,那我们换位思考一下。当线程执行时,发现条件不满足,是不是可以让线程进入等待状态?当条件满足的时候,通知等待的线程重新执行?

也就是说,如果线程需要的条件不满足,我们就让线程进入等待状态;如果线程需要的条件满足时,我们再通知等待的线程重新执行。这样,就能够避免程序进行循环等待进而消耗CPU的问题。

那么,问题又来了!当条件不满足时,如何实现让线程等待?当条件满足时,又如何唤醒线程呢?

不错,这是个问题!不过这个问题解决起来也非常简单。简单的说,就是使用线程的等待与通知机制。

线程的等待与通知机制

我们可以使用线程的等待与通知机制来优化阻止请求与保持条件时,循环获取账户资源的问题。具体的等待与通知机制如下所示。

执行的线程首先获取互斥锁,如果线程继续执行时,需要的条件不满足,则释放互斥锁,并进入等待状态;当线程继续执行需要的条件满足时,就通知等待的线程,重新获取互斥锁。

那么,说了这么多,Java支持这种线程的等待与通知机制吗?其实,这个问题问的就有点废话了,Java这么优秀(牛逼)的语言肯定支持啊,而且实现起来也比较简单。

Java实现线程的等待与通知机制

实现方式

其实,使用Java语言实现线程的等待与通知机制有多种方式,这里我就简单的列举一种方式,其他的方式大家可以自行思考和实现,有不懂的地方也可以问我!

在Java语言中,实现线程的等待与通知机制,一种简单的方式就是使用synchronized并结合wait()、notify()和notifyAll()方法来实现。

实现原理

我们使用synchronized加锁时,只允许一个线程进入synchronized保护的代码块,也就是临界区。如果一个线程进入了临界区,则其他的线程会进入阻塞队列里等待,这个阻塞队列和synchronized互斥锁是一对一的关系,也就是说,一把互斥锁对应着一个独立的阻塞队列。

在并发编程中,如果一个线程获得了synchronized互斥锁,但是不满足继续向下执行的条件,则需要进入等待状态。此时,可以使用Java中的wait()方法来实现。当调用wait()方法后,当前线程就会被阻塞,并且会进入一个等待队列中进行等待,这个由于调用wait()方法而进入的等待队列也是互斥锁的等待队列。而且,线程在进入等待队列的同时,会释放自身获得的互斥锁,这样,其他线程就有机会获得互斥锁,进而进入临界区了。整个过程可以表示成下图所示。

当线程执行的条件满足时,可以使用Java提供的notify()和notifyAll()方法来通知互斥锁等待队列中的线程,我们可以使用下图来简单的表示这个过程。

这里,需要注意如下事项:

(1)使用notify()和notifyAll()方法通知线程时,调用notify()和notifyAll()方法时,满足线程的执行条件,但是当线程真正执行的时候,条件可能已经不再满足了,可能有其他线程已经进入临界区执行。

(2)被通知的线程继续执行时,需要先获取互斥锁,因为在调用wait()方法等待时已经释放了互斥锁。

(3)wait()、notify()和notifyAll()方法操作的队列是互斥锁的等待队列,如果synchronized锁定的是this对象,则一定要使用this.wait()、this.notify()和this.notifyAll()方法;如果synchronized锁定的是target对象,则一定要使用target.wait()、target.notify()和target.notifyAll()方法。

(4)wait()、notify()和notifyAll()方法调用的前提是已经获取了相应的互斥锁,也就是说,wait()、notify()和notifyAll()方法都是在synchronized方法中或代码块中调用的。如果在synchronized方法外或代码块外调用了三个方法,或者锁定的对象是this,使用target对象调用三个方法的话,JVM会抛出java.lang.IllegalMonitorStateException异常。

具体实现

实现逻辑

在实现之前,我们还需要考虑以下几个问题:

  • 选择哪个互斥锁

在之前的程序中,我们在TansferAccount类中,存在一个ResourcesRequester 类的单例对象,所以,我们是可以使用this作为互斥锁的。这一点大家需要重点理解。

  • 线程执行转账操作的条件

转出账户和转入账户都没有被分配过。

  • 线程什么时候进入等待状态

线程继续执行需要的条件不满足的时候,进入等待状态。

  • 什么时候通知等待的线程执行

当存在线程释放账户的资源时,通知等待的线程继续执行。

综上,我们可以得出以下核心代码。

while(不满足条件){     wait(); }

那么,问题来了!为何是在while循环中调用wait()方法呢?因为当wait()方法返回时,有可能线程执行的条件已经改变,也就是说,之前条件是满足的,但是现在已经不满足了,所以要重新检验条件是否满足。

实现代码

我们优化后的ResourcesRequester类的代码如下所示。

public class ResourcesRequester{     //存放申请资源的集合     private List resources = new ArrayList();     //一次申请所有的资源     public synchronized void applyResources(Object source, Object target){         while(resources.contains(source) || resources.contains(target)){             try{                 wait();             }catch(Exception e){                 e.printStackTrace();             }         }         resources.add(source);         resources.add(targer);     }          //释放资源     public synchronized void releaseResources(Object source, Object target){         resources.remove(source);         resources.remove(target);         notifyAll();     } }

生成ResourcesRequester单例对象的Holder类ResourcesRequesterHolder的代码如下所示。

public class ResourcesRequesterHolder{     private ResourcesRequesterHolder(){}          public static ResourcesRequester getInstance(){         return Singleton.INSTANCE.getInstance();     }     private enum Singleton{         INSTANCE;         private ResourcesRequester singleton;         Singleton(){             singleton = new ResourcesRequester();         }         public ResourcesRequester getInstance(){             return singleton;         }     } }

执行转账操作的类的代码如下所示。

public class TansferAccount{     //账户的余额     private Integer balance;     //ResourcesRequester类的单例对象     private ResourcesRequester requester;         public TansferAccount(Integer balance){         this.balance = balance;         this.requester = ResourcesRequesterHolder.getInstance();     }     //转账操作     public void transfer(TansferAccount target, Integer transferMoney){         //一次申请转出账户和转入账户,直到成功         requester.applyResources(this, target))         try{             //对转出账户加锁             synchronized(this){                 //对转入账户加锁                 synchronized(target){                     if(this.balance >= transferMoney){                         this.balance -= transferMoney;                         target.balance += transferMoney;                     }                    }             }         }finally{             //最后释放账户资源             requester.releaseResources(this, target);         }     } }

可以看到,我们在程序中通知处于等待状态的线程时,使用的是notifyAll()方法而不是notify()方法。那notify()方法和notifyAll()方法两者有什么区别呢?

notify()和notifyAll()的区别

  • notify()方法

随机通知等待队列中的一个线程。

  • notifyAll()方法

通知等待队列中的所有线程。

在实际工作过程中,如果没有特殊的要求,尽量使用notifyAll()方法。因为使用notify()方法是有风险的,可能会导致某些线程永久不会被通知到!

"高并发场景下怎么优化加锁方式"的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注网站,小编将为大家输出更多高质量的实用文章!

线程 方法 账户 条件 队列 代码 资源 问题 循环 方式 机制 对象 状态 程序 转账 也就是 时候 过程 也就是说 同时 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 网络安全工作议事例会制度 网络安全画出来 人工智能学习数据库 致家长网络安全的一封信 北京快快网络技术 excel分析数据库下载 南京电子软件开发销售电话 发明专利技术数据库 什么叫做中心站服务器 宁波软件开发新感觉建站 网络安全联合调查组 世界服务器的作用 中山数字软件开发价目表 江西app软件开发流程 网络安全你我共守护手抄报 如何查看数据库置疑 泰州销售管理软件开发平台 b20视频综合管理服务器 智慧工地管理平台软件开发 成绩管理系统数据库论文 数据库自动清理日志有什么危害 网络安全教育班队活动总结 东莞软件开发驻厂机构 广州春种秋收互联网科技公司 西安做浪潮服务器的代理商 我的世界服务器自定义地图怎么画 云服务器供应商怎么选择 江苏省公安厅网络安全总队杨 2019软件开发工作好找吗 对未来网络技术及应用进行展望
0