当前位置: 首页 > 科技观察

如何避免死锁?我们有套路可以按照上篇文章中的

时间:2023-03-15 17:11:40 科技观察

写这么多共享资源,如何用一把锁保护多个资源?文章中我们讲了银行转账的经典案例,有两个问题:单纯使用synchronized方法无法保护目标(itcannotprotectthetarget)。使用Account.class锁方案,锁的粒度过大,导致操作(提现、转账、修改密码等)都会变成串行操作。如何解决这两个问题?我们换好衣服回到过去找银行,透过现象看本质,咚咚咚.....来到银行跟柜员说要转100个铜币到打打儿。在墙上找你和铁蛋儿的账本。这时候出纳可能会面临三种情况:理想情况:你和铁蛋儿的账本都闲着。一起拿回去,账本上减去100铜币。给蛋儿的账本加100铜币,柜员一转身把账本挂回墙上,完成你的生意尴尬:你的账本在这里,铁蛋儿的账本却被其他柜员拿出去转钱对其他人来说,你必须等待其他出纳员归还铁蛋儿的账本很疯狂:你的账本不在,铁蛋儿的账本也不在。只能等两个账本都归还,拖慢柜员的取款操作。他必须先拿到你的账本,然后去LatteEgg的账本。只有获得两个账本后才能完成转账(理想状态)。用程序模型来描述获取账本的过程:下面继续用程序代码来描述上面的模型:classAccount{privateintbalance;//Transfervoidtransfer(Accounttarget,intamt){//Locktransferaccountsynchronized(this){//锁定转账账户synchronized(target){if(this.balance>amt){this.balance-=amt;target.balance+=amt;}}}}}这个方案看起来很好,解决了上面提到的两个问题文章开头,但事实真的如此吗?我们刚才说的理想状态是银行只有一个柜员(单线程)。随着银行规模的扩大,许多账本已经挂在墙上。为了应对繁忙的业务,银行开设了多个窗口。这时有多个柜员(多线程)处理银行的业务。柜员1正在办理给铁蛋儿转账业务,但只拿到了你的账本;柜员2正在办理铁蛋儿给您转账的业务,但只拿到了铁蛋儿的账本。这时候双方都处于尴尬的状态,两个柜员都在等待对方归还账簿为当前客户办理转账业务。现实中柜员是可以交流的,喊出一个声音,铁蛋儿的账本,我先用一下,用完了还给你,只是程序没这么智能。synchronized内置锁是非常持久的,它会告诉你“等一下”的真相,死锁最终还是出现了。Java有synchronized内置锁,发明了显示锁Lock。仅仅是为了治愈同步“死等待”的持久性吗?😏解决方案是如何解决上述问题的?俗话说知己知彼,百战不殆。我们必须先了解什么会导致死锁,然后才能知道如何避免死锁。幸运的是,我们可以站在巨人的肩膀上看问题。Coffman总结了四个条件来说明死锁会发生Situation:Coffmancondition互斥条件:指一个进程独占使用已分配的资源,即某一资源在一段时间内只被一个进程占用。如果此时有其他进程在请求资源,请求者只能等到占用资源的进程用完释放。请求和持有条件:表示一个进程至少保留了一个资源,但是又提出了新的资源请求,并且该资源已经被其他进程占用。此时,请求进程被阻塞,但它仍然持有它已经获得的其他资源。不可让渡条件:指进程已经获得的资源,在用完之前不能剥夺,用完只能自己释放。循环等待条件:发生死锁时,必然存在进程-资源环链,即进程集{P1,P2,...,Pn}中的P1正在等待P2占用的一个资源;P2正在等待一个被P2占用的资源;等待P3占用的资源,...,Pn正在等待P0已经占用的资源。这些条件很容易理解。其中,“互斥条件”是并发编程的基础,这个条件是没有办法改变的。但是其他三个条件可能会发生变化,也就是说,如果其他三个条件都被破坏了,就不会出现上面说的死锁问题。销毁请求并维护条件。每个柜员都可以取放账本,方便互相等待。健康)状况。为了打破请求和保持条件,必须立即获得所有资源。作为一名程序员,你一定听过这样一句话:软件工程中遇到的任何问题,都可以通过增加一个中间层来解决。我们不允许出纳员拿走和放置分类账,分类账必须由单独的分类账管理员管理。也就是说是簿记员取书的临界区。如果你只拿到其中一本书,你不会把它交给柜员,而是等待柜员询问下一次两本书是否都在。Objectto){if(getallbooks){returntrue;}else{returnfalse;}}//返回资源synchronizedvoidreleaseObtainedAccountBook(Objectfrom,Objectto){返回获取的账本}}publicclassAccount{//单例账本管理器privateAccountBookManageraccountBookManager;publicvoidtransfer(Accounttarget,intamt){//一次性申请转出转出账号直到成功while(!accountBookManager.getAllRequiredAccountBook(this,target)){return;}try{//锁定转出账号synchronized(this){//锁转账synchronized(target){if(this.balance>amt){this.balance-=amt;target.balance+=amt;}}}}finally{accountBookManager.releaseObtainedAccountBook(this,target);}}}Destroytheinalienablecondition上面已经给了你一点提示。为了解决内置锁的持久化,Java显示锁支持通知(notify/notifyall)和等待(wait),也就是说可以实现这个功能。铁,铁蛋儿的账本,我先用一下,用完了还给你。这个在JavaSDK相关内容中会有说明。破坏循环等待条件也很简单。我们只需要排序获取资源序号就可以解决这个问题,去掉循环继续用代码来说明:classAccount{privateintid;privateintbalance;//转账voidtransfer(Accounttarget,intamt){Accountsmaller=thisAccountlarger=target;//排序if(this.id>target.id){smaller=target;larger=this;}//锁定序列号numbersmallsynchronized(smaller){//锁定序列号大的账户synchronized(larger){if(this.balance>amt){this.balance-=amt;target.balance+=amt;}}}}}when占用越小,其他线程就会被阻塞,不会出现死锁。补充说明在实际业务中,Account会是一个数据库对象,我们可以通过事务或者数据库的乐观锁来解决另外,在分布式系统中,账本管理员角色的处理也可能用redis分布式锁来解决。在处理销毁请求和持有条件时,我们使用while循环的方式不断请求锁。另外在实际业务中,我们还会有超时设置,防止无休止的浪费CPU使用率。另外可以尝试使用阿里的开源工具Arthas查看CPU使用率、线程等相关问题。github上有明确的说明总结了计算机的计算能力。超过人类,但他的智慧还有待提高。在看并发问题时,我们常常认为计算机也可以做人与人之间最基本的通信。事实上,并非如此。还是那句话。编写并发程序,必须站在计算机的立足点上。我们不提倡粗粒度的锁,所以我们会使用细粒度的锁,但是在使用细粒度的锁的时候,一定要严格按照考夫曼的四大条件来一一判断,然后应用我们的解决方案来解决问题。足够的。当灵魂询问销毁请求和维护条件时,处理能力的瓶颈在于账本管理员。你觉得这种处理方式会增加并发吗?破坏requesthold条件的方法和破坏loopwait的方法,你觉得哪种方法更好在破坏requesthold条件的时候,代码改成下面这样会怎么样?publicvoidtransfer(Accounttarget,intamt){//一次性申请转出转账直到成功while(accountBookManager.getAllRequiredAccountBook(this,target)){}try{//锁定转出的账户synchronized(this){//锁转账synchronized(target){if(this.balance>amt){this.balance-=amt;target.balance+=amt;}}}}finally{accountBookManager.releaseObtainedAccountBook(this,target);}}}