当前位置:首页 > 公众号精选 > 架构师社区
[导读]来自:冰河技术      写在前面 很多时候,我们在并发编程中,涉及到加锁操作时,对代码块的加锁操作真的合理吗?还有没有需要优化的地方呢? 前言 在《【高并发】优化加锁方式时竟然死锁了!!》一文中,我们介绍了产生死锁时的四个必要条件,只有四个条件同

【高并发】高并发场景下如何优化加锁方式?看完这篇我确实明白了!!

来自:冰河技术     

写在前面

很多时候,我们在并发编程中,涉及到加锁操作时,对代码块的加锁操作真的合理吗?还有没有需要优化的地方呢?

前言

在《【高并发】优化加锁方式时竟然死锁了!!》一文中,我们介绍了产生死锁时的四个必要条件,只有四个条件同时具备时才能发生死锁。其中,我们在阻止请求与保持条件时,采用了一次性申请所有的资源的方式。例如在我们完成转账操作的过程中,我们一次性申请账户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<Object> resources = new ArrayList<Object>();
    //一次申请所有的资源
    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()方法是有风险的,可能会导致某些线程永久不会被通知到!

写在最后

最后,附上并发编程需要掌握的核心技能知识图,祝大家在学习并发编程时,少走弯路。

【高并发】高并发场景下如何优化加锁方式?看完这篇我确实明白了!!

特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:

【高并发】高并发场景下如何优化加锁方式?看完这篇我确实明白了!!

长按订阅更多精彩▼

【高并发】高并发场景下如何优化加锁方式?看完这篇我确实明白了!!

如有收获,点个在看,诚挚感谢

免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭