Directrices de codificación de seguridad de Java: visibilidad y atomicidad

Introducción

Muchas variables se definen en una clase Java. Hay variables de clase y variables de instancia. Estas variables encontrarán algunos problemas de visibilidad y atomicidad durante el proceso de acceso. Aquí echamos un vistazo más de cerca a cómo evitar estos problemas.

Visibilidad de objetos inmutables

Un objeto inmutable es un objeto que no se puede modificar después de la inicialización, entonces, ¿se introduce un objeto inmutable en la clase y todas las modificaciones a un objeto inmutable son inmediatamente visibles para todos los hilos?

De hecho, los objetos inmutables solo pueden garantizar la seguridad del uso de objetos en un entorno multiproceso, pero no la visibilidad de los objetos.

Analicemos primero la variabilidad. Consideremos el siguiente ejemplo:

public final class ImmutableObject {
    
    
    private final int age;
    public ImmutableObject(int age){
    
    
        this.age=age;
    }
}

Definimos un objeto ImmutableObject, la clase es final y el único campo dentro también es final. Entonces, este ImmutableObject no se puede cambiar después de la inicialización.

Luego definimos una clase para obtener y establecer este ImmutableObject:

public class ObjectWithNothing {
    
    
    private ImmutableObject refObject;
    public ImmutableObject getImmutableObject(){
    
    
        return refObject;
    }
    public void setImmutableObject(int age){
    
    
        this.refObject=new ImmutableObject(age);
    }
}

En el ejemplo anterior, definimos un refObject para un objeto inmutable, y luego definimos los métodos get y set.

Tenga en cuenta que aunque la clase ImmutableObject en sí es inmutable, nuestra referencia al objeto refObject es mutable. Esto significa que podemos llamar al método setImmutableObject varias veces.

Volvamos a hablar de visibilidad.

En el ejemplo anterior, en un entorno de subprocesos múltiples, ¿getImmutableObject devolverá un nuevo valor cada vez que setImmutableObject?

la respuesta es negativa.

Cuando se compila el código fuente, el orden de las instrucciones generadas en el compilador no es exactamente el mismo que el orden del código fuente. El procesador puede ejecutar instrucciones de forma desordenada o paralela (siempre que el resultado final de la ejecución del programa sea coherente con el resultado de la ejecución en un entorno serial estricto en la JVM, esta reordenación está permitida). Y el procesador también tiene una caché local.Cuando los resultados se almacenan en la caché local, otros hilos no pueden ver los resultados. Además, el orden en el que se envían las cachés a la memoria principal también puede cambiar.

¿Cómo resolverlo?

La solución más simple para la visibilidad es agregar la palabra clave volátil, que puede usar la regla de suceder antes del modelo de memoria de Java para garantizar que las modificaciones de las variables volátiles sean visibles para todos los subprocesos.

public class ObjectWithVolatile {
    
    
    private volatile ImmutableObject refObject;
    public ImmutableObject getImmutableObject(){
    
    
        return refObject;
    }
    public void setImmutableObject(int age){
    
    
        this.refObject=new ImmutableObject(age);
    }
}

Además, se puede lograr el mismo efecto utilizando el mecanismo de bloqueo:

public class ObjectWithSync {
    
    
    private  ImmutableObject refObject;
    public synchronized ImmutableObject getImmutableObject(){
    
    
        return refObject;
    }
    public synchronized void setImmutableObject(int age){
    
    
        this.refObject=new ImmutableObject(age);
    }
}

Finalmente, también podemos usar clases atómicas para lograr el mismo efecto:

public class ObjectWithAtomic {
    
    
    private final AtomicReference<ImmutableObject> refObject= new AtomicReference<>();
    public ImmutableObject getImmutableObject(){
    
    
        return refObject.get();
    }
    public void setImmutableObject(int age){
    
    
        refObject.set(new ImmutableObject(age));
    }
}

Asegurar la atomicidad de las operaciones compuestas de variables compartidas

Si es un objeto compartido, entonces debemos considerar la atomicidad en un entorno multiproceso. Si es una operación compuesta en variables compartidas, como: ++, - * =, / =,% =, + =, - =, << =, >> =, >>> =, ^ = etc., parece Una declaración, pero en realidad es una colección de múltiples declaraciones.

Debemos considerar la seguridad del subproceso múltiple.

Considere el siguiente ejemplo:

public class CompoundOper1 {
    
    
    private int i=0;
    public int increase(){
    
    
        i++;
        return i;
    }
}

En el ejemplo, acumulamos int i. Pero ++ en realidad se compone de tres operaciones:

  1. Lea el valor de i de la memoria y escríbalo en el registro de la CPU.
  2. El valor de i + 1 en el registro de la CPU
  3. Escriba el valor de nuevo en i en la memoria.

Si en un entorno de un solo subproceso, no hay ningún problema, pero en un entorno de varios subprocesos, debido a que no es una operación atómica, pueden surgir problemas.

Hay muchas soluciones, la primera es usar la palabra clave sincronizada

    public synchronized int increaseSync(){
    
    
        i++;
        return i;
    }

El segundo es usar el bloqueo:

    private final ReentrantLock reentrantLock=new ReentrantLock();

    public int increaseWithLock(){
    
    
        try{
    
    
            reentrantLock.lock();
            i++;
            return i;
        }finally {
    
    
            reentrantLock.unlock();
        }
    }

El tercero es utilizar la clase atómica Atómica:

    private AtomicInteger atomicInteger=new AtomicInteger(0);

    public int increaseWithAtomic(){
    
    
        return atomicInteger.incrementAndGet();
    }

Asegurar la atomicidad de múltiples operaciones atómicas atómicas

Si un método usa múltiples operaciones atómicas, aunque una sola operación atómica es atómica, la combinación no lo es necesariamente.

Veamos un ejemplo:

public class CompoundAtomic {
    
    
    private AtomicInteger atomicInteger1=new AtomicInteger(0);
    private AtomicInteger atomicInteger2=new AtomicInteger(0);

    public void update(){
    
    
        atomicInteger1.set(20);
        atomicInteger2.set(10);
    }

    public int get() {
    
    
        return atomicInteger1.get()+atomicInteger2.get();
    }
}

En el ejemplo anterior, definimos dos AtomicIntegers y operamos en los dos AtomicIntegers en las operaciones de actualización y obtención, respectivamente.

Aunque AtomicInteger es atómico, la combinación de dos AtomicIntegers diferentes no lo es. Puede encontrar problemas durante operaciones multiproceso.

Del mismo modo, podemos utilizar mecanismos de sincronización o bloqueos para garantizar la coherencia de los datos.

Asegurar la atomicidad de la cadena de llamadas al método

Si queremos crear una instancia de un objeto, y la instancia de este objeto se crea mediante llamadas encadenadas. Entonces necesitamos asegurar la atomicidad de las llamadas en cadena.

Considere el siguiente ejemplo:

public class ChainedMethod {
    
    
    private int age=0;
    private String name="";
    private String adress="";

    public ChainedMethod setAdress(String adress) {
    
    
        this.adress = adress;
        return this;
    }

    public ChainedMethod setAge(int age) {
    
    
        this.age = age;
        return this;
    }

    public ChainedMethod setName(String name) {
    
    
        this.name = name;
        return this;
    }
}

Un objeto muy simple, definimos tres propiedades, cada conjunto devolverá una referencia a este.

Veamos cómo llamar en un entorno de subprocesos múltiples:

        ChainedMethod chainedMethod= new ChainedMethod();
        Thread t1 = new Thread(() -> chainedMethod.setAge(1).setAdress("www.flydean.com1").setName("name1"));
        t1.start();

        Thread t2 = new Thread(() -> chainedMethod.setAge(2).setAdress("www.flydean.com2").setName("name2"));
        t2.start();

Porque en un entorno multiproceso, el método de configuración anterior puede resultar confuso.

¿Cómo resolverlo? Primero podemos crear una copia local, que es segura para subprocesos porque se accede localmente, y finalmente copiar la copia en el objeto de instancia recién creado.

El código principal es el siguiente:

public class ChainedMethodWithBuilder {
    
    
    private int age=0;
    private String name="";
    private String adress="";

    public ChainedMethodWithBuilder(Builder builder){
    
    
        this.adress=builder.adress;
        this.age=builder.age;
        this.name=builder.name;
    }

    public static class Builder{
    
    
        private int age=0;
        private String name="";
        private String adress="";

        public static Builder newInstance(){
    
    
            return new Builder();
        }
        private Builder() {
    
    }

        public Builder setName(String name) {
    
    
            this.name = name;
            return this;
        }

        public Builder setAge(int age) {
    
    
            this.age = age;
            return this;
        }

        public Builder setAdress(String adress) {
    
    
            this.adress = adress;
            return this;
        }

        public ChainedMethodWithBuilder build(){
    
    
            return new ChainedMethodWithBuilder(this);
        }
    }

Veamos cómo llamar:

      final ChainedMethodWithBuilder[] builder = new ChainedMethodWithBuilder[1];
        Thread t1 = new Thread(() -> {
    
    
            builder[0] =ChainedMethodWithBuilder.Builder.newInstance()
                .setAge(1).setAdress("www.flydean.com1").setName("name1")
                .build();});
        t1.start();

        Thread t2 = new Thread(() ->{
    
    
            builder[0] =ChainedMethodWithBuilder.Builder.newInstance()
                .setAge(1).setAdress("www.flydean.com1").setName("name1")
                .build();});
        t2.start();

Debido a que las variables utilizadas en la expresión lambda deben ser final o equivalente final, necesitamos construir una matriz final.

Leer y escribir valor de 64 bits

En Java, 64 bits de longitud y doble se tratan como dos 32 bits.

Por lo tanto, una operación de 64 bits se divide en dos operaciones de 32 bits. Esto conduce a problemas de atomicidad.

Considere el siguiente código:

public class LongUsage {
    
    
    private long i =0;

    public void setLong(long i){
    
    
        this.i=i;
    }
    public void printLong(){
    
    
        System.out.println("i="+i);
    }
}

Debido a que la lectura y escritura de long se divide en dos partes, si los métodos setLong y printLong se llaman varias veces en un entorno de subprocesos múltiples, pueden surgir problemas.

La solución es simple, simplemente defina las variables largas o dobles como volátiles.

private volatile long i = 0;

El código de este artículo:

aprender-java-base-9-to-20 / tree / master / security

Este artículo se ha incluido en http://www.flydean.com/java-security-code-line-visibility-atomicity/

¡La interpretación más popular, los productos secos más profundos, el tutorial más conciso y muchos consejos que no sabes te esperan para descubrir!

Bienvenido a prestar atención a mi cuenta oficial: "programa esas cosas", conoce la tecnología, ¡conocerte mejor!

Supongo que te gusta

Origin blog.csdn.net/superfjj/article/details/108791925
Recomendado
Clasificación