¡Cómo reducir la granularidad de los bloqueos en la programación concurrente y cómo optimizar el rendimiento!

El rendimiento es muy importante en aplicaciones multiproceso de alta carga. Para lograr un mejor rendimiento, los desarrolladores deben ser conscientes de la importancia de la concurrencia. Cuando necesitamos utilizar la concurrencia, a menudo hay un recurso que deben compartir dos o más subprocesos.

En este caso, hay una condición de carrera, es decir, un subproceso puede obtener el bloqueo (el bloqueo está vinculado a un recurso específico), y otros subprocesos que desean obtener el bloqueo se bloquearán. La implementación de este mecanismo de sincronización tiene un precio, para brindarle un modelo de sincronización útil, tanto la JVM como el sistema operativo consumen recursos. Hay tres factores más importantes que hacen que la implementación concurrente consuma muchos recursos. Son:

  • Cambio de contexto
  • Sincronización de memoria
  • Bloqueo Para escribir código optimizado para la sincronización, debe reconocer estos tres factores y cómo reducirlos. Hay muchas cosas a las que debe prestar atención al escribir dicho código. En este artículo, le presentaré una técnica que reduce la granularidad de bloqueo para reducir estos factores.

Comencemos con un principio básico: no mantengas candados innecesarios durante mucho tiempo.

Haga todo lo necesario antes de adquirir el bloqueo, utilice el bloqueo solo en los recursos que deben sincronizarse y libérelo inmediatamente después de su uso. Veamos un ejemplo simple:

public class HelloSync {
    private Map dictionary = new HashMap();
    public synchronized void borringDeveloper(String key, String value) {
        long startTime = (new java.util.Date()).getTime();
        value = value + "_"+startTime;
        dictionary.put(key, value);
        System.out.println("I did this in "+
     ((new java.util.Date()).getTime() - startTime)+" miliseconds");
    }
}

En este ejemplo, violamos el principio básico porque creamos dos objetos Date, llamados System.out.println (), e hicimos muchas operaciones de concatenación de cadenas, pero la única operación de sincronización requerida fue "dictionary. poner (clave, valor); ". Modifiquemos el código para cambiar el método de sincronización en un bloque de sincronización que contenga solo esta oración y obtengamos el siguiente código más optimizado:

public class HelloSync {
    private Map dictionary = new HashMap();
    public void borringDeveloper(String key, String value) {
        long startTime = (new java.util.Date()).getTime();
        value = value + "_"+startTime;
        synchronized (dictionary) {
            dictionary.put(key, value);
        }
        System.out.println("I did this in "+
 ((new java.util.Date()).getTime() - startTime)+" miliseconds");
    }
}

El código anterior se puede optimizar aún más, pero solo quiero transmitir esta idea. Si está interesado en cómo optimizar aún más, consulte java.util.concurrent.ConcurrentHashMap.

Entonces, ¿cómo reducimos la granularidad del bloqueo? En pocas palabras, solicitando la menor cantidad de candados posible. La idea básica es usar diferentes bloqueos para proteger múltiples variables de estado independientes en la misma clase, en lugar de usar solo un bloqueo para todo el dominio de la clase. Echemos un vistazo al siguiente ejemplo simple que he visto en muchas aplicaciones:

public class Grocery {
    private final ArrayList fruits = new ArrayList();
    private final ArrayList vegetables = new ArrayList();
    public synchronized void addFruit(int index, String fruit) {
        fruits.add(index, fruit);
    }
    public synchronized void removeFruit(int index) {
        fruits.remove(index);
    }
    public synchronized void addVegetable(int index, String vegetable) {
        vegetables.add(index, vegetable);
    }
    public synchronized void removeVegetable(int index) {
        vegetables.remove(index);
    }
}

El dueño de la tienda de comestibles puede agregar / quitar las verduras y frutas en su tienda de comestibles. La implementación anterior de la tienda de comestibles utiliza el candado Grocery básico para proteger frutas y verduras, porque la sincronización se realiza en el dominio del método. De hecho, en lugar de usar este candado a gran escala, podemos usar un candado para cada recurso (frutas y verduras). Eche un vistazo al código mejorado:

public class Grocery {
    private final ArrayList fruits = new ArrayList();
    private final ArrayList vegetables = new ArrayList();
    public void addFruit(int index, String fruit) {
        synchronized(fruits) fruits.add(index, fruit);
    }
    public void removeFruit(int index) {
        synchronized(fruits) {fruits.remove(index);}
    }
    public void addVegetable(int index, String vegetable) {
        synchronized(vegetables) vegetables.add(index, vegetable);
    }
    public void removeVegetable(int index) {
        synchronized(vegetables) vegetables.remove(index);
    }
}

Después de usar dos cerraduras (separe las cerraduras), encontraremos que hay menos situaciones de bloqueo de cerraduras que cuando se usaba una cerradura completa antes. Cuando aplicamos esta técnica a bloqueos con contención de bloqueo moderada, la mejora de optimización será más obvia. Si este método se aplica a cerraduras con una ligera contención de cerraduras, la mejora es relativamente pequeña pero sigue siendo eficaz. Pero si lo usa en cerraduras con fuerte contención de cerraduras, debe darse cuenta de que los resultados no siempre son mejores.

Utilice esta técnica de forma selectiva. Si sospecha que un bloqueo es un bloqueo de contención fuerte, utilice el siguiente método para confirmar si desea utilizar la técnica anterior:

  • Confirme cuánta mezcla tendrá su producto y multiplique la mezcla por tres o cinco veces (o incluso 10 veces, si quiere ser infalible)
  • Realice las pruebas apropiadas basadas en esta afirmación
  • Compare los resultados de las pruebas de los dos programas y luego seleccione el más adecuado.

Existen muchas técnicas para mejorar el rendimiento de la sincronización, pero para todas las técnicas, solo hay un principio básico: no mantener bloqueos innecesarios durante mucho tiempo.

Este principio básico puede entenderse como "solicitar el menor número posible de bloqueos", como ya te expliqué antes, y también puede haber otras explicaciones (métodos de implementación), que introduciré en artículos posteriores.

expediente

Supongo que te gusta

Origin blog.csdn.net/weixin_46577306/article/details/108087799
Recomendado
Clasificación