写点什么

高并发场景下优化加锁方式:线程等待与通知机制

  • 2022 年 3 月 26 日
  • 本文字数:3674 字

    阅读完需:约 12 分钟

本文分享自华为云社区《【高并发】讲讲高并发场景下如何优化加锁方式?》,作者: 冰 河 。


互斥条件、不可剥夺条件请求与保持条件、循环等待条件,这是产生死锁时的四个必要条件,只有四个条件同时具备时才能发生死锁。其中,我们在阻止请求与保持条件时,采用了一次性申请所有的资源的方式。例如在我们完成转账操作的过程中,我们一次性申请账户 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()方法是有风险的,可能会导致某些线程永久不会被通知到!


点击关注,第一时间了解华为云新鲜技术~​

发布于: 刚刚阅读数: 2
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
高并发场景下优化加锁方式:线程等待与通知机制_线程_华为云开发者社区_InfoQ写作平台