Modo de bloqueo optimizado en escenarios de alta concurrencia: mecanismo de notificación y espera de subprocesos

Este artículo se comparte desde Huawei Cloud Community " [Alta concurrencia] ¿Cómo optimizar el método de bloqueo en escenarios de alta concurrencia? ", Autor: Glaciar.

Muchas veces, cuando estamos involucrados en operaciones de bloqueo en programación concurrente, ¿es realmente razonable la operación de bloqueo de bloques de código? ¿Hay algo más que deba optimizarse?

Las condiciones de exclusión mutua, las condiciones inalienables, las condiciones de solicitud y retención y las condiciones de espera circular son las cuatro condiciones necesarias para los interbloqueos. Los interbloqueos solo pueden ocurrir cuando se cumplen las cuatro condiciones al mismo tiempo. Entre ellos, cuando bloqueamos la solicitud y mantenemos las condiciones, usamos el método de solicitar todos los recursos a la vez. Por ejemplo, en el proceso de completar la operación de transferencia, solicitamos la cuenta A y la cuenta B al mismo tiempo, y luego realizamos la operación de transferencia después de que ambas cuentas se hayan aplicado con éxito. En el método de transferencia que implementamos, se utiliza un bucle infinito para obtener recursos de forma cíclica hasta obtener al mismo tiempo la cuenta A y la cuenta B. El código central es el siguiente.

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

Si el tiempo de ejecución del método applyResources() de la clase ResourcesRequester es muy corto, y el conflicto causado por la concurrencia del programa no es grande, y el programa se repite de varias a docenas de veces, la cuenta de transferencia y la transferencia -en cuenta se puede obtener al mismo tiempo, este esquema es factible.

Sin embargo, si el tiempo de ejecución del método applyResources() de la clase ResourcesRequester es relativamente largo, o si el conflicto causado por la concurrencia del programa es relativamente grande, en este momento, puede llevar miles de ciclos obtener la cuenta de transferencia y la transferencia. -en cuenta al mismo tiempo. . Esto consume demasiados recursos de la CPU y, en este momento, esta solución no es factible.

Entonces, ¿hay alguna forma de optimizar este esquema?

análisis del problema

Como hay un problema con la solución de usar un ciclo infinito para obtener recursos todo el tiempo, pensemos en ello. Cuando se ejecuta el hilo, se encuentra que la condición no se cumple, ¿puede el hilo entrar en el estado de espera? Cuando se cumpla la condición, ¿notificar al subproceso en espera para volver a ejecutar?

Es decir, si no se cumplen las condiciones requeridas por el hilo, dejamos que el hilo entre en estado de espera, si se cumplen las condiciones requeridas por el hilo, le avisamos al hilo en espera para que vuelva a ejecutar. De esta manera, es posible evitar el problema de que el programa realiza una espera cíclica y consume la CPU.

Bueno, aquí viene el problema de nuevo! ¿Cómo hacer que el hilo espere cuando no se cumple la condición? Cuando se cumplen las condiciones, ¿cómo despertar el hilo?

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

线程的等待与通知机制

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

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

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

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

实现方式

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

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

实现原理

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

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

imagen

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

imagen

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

(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()方法

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

  • método notificar a todos ()

Notifica a todos los subprocesos en la cola de espera.

En el proceso de trabajo real, si no hay requisitos especiales, intente utilizar el método notificar a todos(). ¡Debido a que usar el método de notificación () es arriesgado, es posible que algunos subprocesos nunca sean notificados!

Haga clic en Seguir para conocer las nuevas tecnologías de HUAWEI CLOUD por primera vez~

Supongo que te gusta

Origin juejin.im/post/7079235599235809317
Recomendado
Clasificación