Java Multithreading (3): problemas de seguridad de subprocesos y soluciones

​¡Crear un hábito de escritura juntos! Este es el octavo día de mi participación en el "Nuggets Daily New Plan · April Update Challenge", haz clic para ver los detalles del evento .

1. ¿Qué es la seguridad de subprocesos?

        Si el resultado del código que se ejecuta en el entorno de subprocesos múltiples está en línea con nuestras expectativas, es decir, el resultado debe estar en el entorno de un solo subproceso, se dice que el programa es seguro para subprocesos . Subproceso no seguro significa que el resultado de la ejecución de un programa en múltiples subprocesos no cumple con las expectativas.

2. Factores que provocan inseguridad en los subprocesos

2.1 Ejecución preventiva

 

2.2 Múltiples subprocesos modifican la misma variable al mismo tiempo

public class ThreadDemo16 {
    static class Counter {
        // 变量
        private int number = 0;
        // 循环次数
        private int count;

        public Counter(int count) {
            this.count = count;
        }

        // ++方法
        public int increment() {
            int tmp = 0;
            for (int i = 0; i < count; i++) {
                tmp++;
            }
            return tmp;
        }

        // --方法
        public int decrement() {
            int tmp = 0;
            for (int i = 0; i < count; i++) {
                tmp--;
            }
            return tmp;
        }

        public int getNumber() {
            return number;
        }
    }

    static int num1 = 0;
    static int num2 = 0;

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter(100000);

        Thread thread1 = new Thread(() -> num1 = counter.increment());

        Thread thread2 = new Thread(() -> num2 = counter.decrement());

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("最终结果:" + (num1 + num2));
    }
}
复制代码

resultado de la operación:

​ 

 

2.3 Operaciones no atómicas

 ¿Qué es la atomicidad?

        Pensamos en una pieza de código como una habitación, y cada hilo es la persona que quiere ingresar a la habitación. Si no hay un mecanismo para asegurar que después de que A entra en la habitación, no ha salido, B también puede entrar en la habitación e interrumpir la privacidad de A en la habitación. Esto no es atómico.

 Una declaración de Java no es necesariamente atómica, ni es necesariamente solo una instrucción.

****Por ejemplo, el n++ que acabamos de ver en realidad se compone de tres pasos:

  1. Leer datos de la memoria a la CPU
  2. hacer actualización de datos
  3. volver a escribir datos en la CPU

¿Qué problemas trae la atomicidad no garantizada a los subprocesos múltiples? 

        Si un hilo está operando sobre una variable, y otros hilos intervienen en el medio, si la operación se interrumpe, el resultado puede ser erróneo. Esto también está estrechamente relacionado con la programación preventiva de subprocesos.Si el subproceso no es "preventivo", incluso si no hay atomicidad, no es un gran problema. 

2.4 Problemas de visibilidad de la memoria

        Visibilidad significa que los cambios en el valor de una variable compartida por un subproceso pueden ser vistos por otros subprocesos de manera oportuna.

public class ThreadDemo17 {
    private static boolean flag = true;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("线程1:开始执行" + LocalDateTime.now());
            while (flag) {
            }
            System.out.println("线程1:结束执行" + LocalDateTime.now());
        });
        thread1.start();

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程2:修改flag = false" + LocalDateTime.now());
            flag = false;
        });
        thread2.start();

    }
}
复制代码

 resultado de la operación:

Se puede ver que el subproceso 2 modifica el indicador a falso y el subproceso 1 nunca finaliza su ejecución, que es el problema de visibilidad de la memoria.

 

2.5 Reordenación de instrucciones

¿Qué es el reordenamiento de instrucciones?

Por ejemplo, un fragmento de código como este:

  1. Ve a la recepción y retira el disco U.
  2. Ir al salón de clases y escribir una tarea de 10 minutos
  3. Ve a la recepción para recoger al mensajero.

        如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序 。 编译器优化的本质是调整代码的执行顺序,在单线程下没问题,但在多线程下容易出现混乱,从而造成线程安全问题。 

那么有这么多线程不安全问题,该如何解决呢?

 

 

3. 解决线程不安全问题

3.1 volatile 解决内存可见性和指令重排序问题

        volatile 可以解决内存可见性指令重排序的问题,代码在写入 volatile 修饰的变量的时候: 

  • 改变线程⼯作内存中volatile变量副本的值;
  • 将改变后的副本的值从⼯作内存刷新到主内存。

代码在读取 volatile 修饰的变量的时候:

  • 从主内存中读取volatile变量的最新值到线程的⼯作内存中;
  • 从⼯作内存中读取volatile变量的副本。

注意 :直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不⼀致的情况,加上 volatile ,强制读写内存,速度虽然慢了,但是数据变得更准确了。

volatile 演示:

public class ThreadDemo17 {
    private volatile static boolean flag = true;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("线程1:开始执行" + LocalDateTime.now());
            while (flag) {
            }
            System.out.println("线程1:结束执行" + LocalDateTime.now());
        });
        thread1.start();

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程2:修改flag = false" + LocalDateTime.now());
            flag = false;
        });
        thread2.start();

    }
}
复制代码

运行结果:

 给之前的代码加上 volatile 之后,线程1接收到了flag的改变,从而结束了执行,解决了内存可见性问题。

volatile 缺点

        volatile 虽然可以解决内存可见性和指令重排序的问题,但是解决不了原子性问题,因此对于 ++ 和 --操作的线程非安全问题依然解决不了,比如以下代码:

public class ThreadDemoVolatile {
    static class Counter {
        // 变量
        private volatile int number = 0;

        // 循环次数
        private final int MAX_COUNT;

        public Counter(int MAX_COUNT) {
            this.MAX_COUNT = MAX_COUNT;
        }

        // ++ 方法
        public void increase() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number++;
            }
        }

        // -- 方法
        public void desc() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number--;
            }
        }

        public int getNumber() {
            return number;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter(100000);
        Thread thread1 = new Thread(counter::increase);
        thread1.start();
        Thread thread2 = new Thread(counter::desc);
        thread2.start();

        // 等待线程执行完成
        thread1.join();
        thread2.join();
        System.out.println("最终结果:" + counter.getNumber());
    }
}
复制代码

 

3.2 锁(synchronized 和 lock)

3.2.1 synchronized

synchronized 基本用法:

  1. 修饰静态方法

    public class ThreadSynchronized {
    
        private static int number = 0;
    
        static class Counter {
            // 循环次数
            private static final int count = 100000;
    
            // ++方法
            public synchronized static void increase() {
                for (int i = 0; i < count; i++) {
                    number++;
                }
            }
    
            // --方法
            public synchronized static void desc() {
                for (int i = 0; i < count; i++) {
                    number--;
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(Counter::increase);
            thread1.start();
            Thread thread2 = new Thread(Counter::desc);
            thread2.start();
    
            // 等待线程执行完毕
            thread1.join();
            thread2.join();
            System.out.println("执行结果:" + number);
        }
    }
    复制代码

    ​​

     

  2. 修饰普通⽅法

    public class ThreadSynchronized2 {
        private static int number = 0;
    
        static class Counter {
            private static final int count = 100000;
    
            // ++方法
            public synchronized void increase() {
                for (int i = 0; i < count; i++) {
                    number++;
                }
            }
    
            // --方法
            public synchronized void desc() {
                for (int i = 0; i < count; i++) {
                    number--;
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Counter counter = new Counter();
            Thread thread1 = new Thread(counter::increase);
            thread1.start();
            Thread thread2 = new Thread(counter::desc);
            thread2.start();
    
            // 等待线程执行完毕
            thread1.join();
            thread2.join();
            System.out.println("最终结果:" + number);
        }
    }
    复制代码

    ​​

     

  3. 修饰代码块

    public class ThreadSynchronized3 {
        private static int number = 0;
    
        static class Counter {
            private static final int count = 100000;
    
            // 自定义锁对象
            final Object myLock = new Object();
    
            // ++方法
            public void increase() {
                for (int i = 0; i < count; i++) {
                    synchronized (myLock) {
                        number++;
                    }
                }
            }
    
            // --方法
            public void desc() {
                for (int i = 0; i < count; i++) {
                    synchronized (myLock) {
                        number--;
                    }
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Counter counter = new Counter();
            Thread thread1 = new Thread(counter::increase);
            thread1.start();
            Thread thread2 = new Thread(counter::desc);
            thread2.start();
    
            thread1.join();
            thread2.join();
            System.out.println("最终结果:" + number);
        }
    }
    复制代码

    ​​

synchronized 特性:

1. 互斥。synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同⼀个对象 synchronized 就会 阻塞等待。

  • 进入 synchronized 修饰的代码块, 相当于 加锁,
  • 退出 synchronized 修饰的代码块, 相当于 解锁。

2. 刷新内存。 synchronized 的⼯作过程: 

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到⼯作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

所以 synchronized 也能保证内存可见性.

​3. 可重入。 synchronized 同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题。

public class ThreadSynchronized4 {
    public static void main(String[] args) {
        synchronized (ThreadSynchronized4.class) {
            System.out.println("主线程得到锁");
            synchronized (ThreadSynchronized4.class) {
                System.out.println("主线程再次得到锁");
            }
        }
    }
}
复制代码

 

注意 :

  1.  加同一把锁。

    public class ThreadSynchronized6 {
        private static final int count = 100000;
        static int num = 0;
    
        public static void main(String[] args) throws InterruptedException {
            Object obj = new Object();
            Object obj2 = new Object();
            Thread t1 = new Thread(() -> {
                synchronized (obj) {
                    for (int i = 0; i < count; i++) {
                        num++;
                    }
                }
            }, "线程1");
            t1.start();
            Thread t2 = new Thread(() -> {
                synchronized (obj2) {
                    for (int i = 0; i < count; i++) {
                        num--;
                    }
                }
            }, "线程2");
            t2.start();
            t1.join();
            t2.join();
            System.out.println("最终执⾏结果:" + num);
        }
    }
    复制代码

  2. 实例类可以使用 this,静态类使用 xxx.class。

  3. synchronized用的锁是存在Java对象头的:


  4. 上⼀个线程解锁之后, 下⼀个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这也就是操作系统线程调度的⼀部分工作.

  5. 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不⼀定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

3.3.2 ****Lock

Uso básico de Lock:

public class ThreadLock {
    public static void main(String[] args) {
        // 1.创建锁对象
        Lock lock = new ReentrantLock();
        // 2.加锁
        lock.lock();
        try {
            // 业务代码
            System.out.println("hello");
        } finally {
            lock.unlock();
        }
    }
}
复制代码

Notas de bloqueo:

  • lock() debe colocarse antes del intento, o la primera línea del intento, de lo contrario, causará dos problemas:
    1. Si se coloca en el intento, el bloqueo fallará debido a una excepción en el código del intento, y el También se realizará la operación de desbloqueo final de la cerradura.
    2. La excepción de desbloqueo cubrirá la excepción comercial en el intento, lo que dificultará la solución de errores.
  • unlock() debe colocarse dentro finalmente.

Bloqueo de bloqueo justo y bloqueo injusto:

  • Bloqueo justo : Múltiples subprocesos adquieren bloqueos en el orden en que solicitan bloqueos. Los subprocesos ingresarán directamente a la cola en cola, y el primer lugar en la cola siempre puede obtener el bloqueo.
  • Bloqueo injusto : cuando varios subprocesos adquieren un bloqueo, intentarán adquirirlo directamente. Si no pueden adquirirlo, ingresarán a la cola de espera. Si pueden adquirirlo, adquirirán el bloqueo directamente.

En el código fuente, puede ver que Lock crea un bloqueo injusto de forma predeterminada. Pasar verdadero creará un bloqueo justo.

Bloqueo VS sincronizado:

  • Lock es más flexible y tiene más métodos.
  • Los tipos de bloqueo son diferentes. El bloqueo es un bloqueo injusto de forma predeterminada, pero también se puede especificar como un bloqueo justo; sincronizado solo puede ser un bloqueo injusto.
  • Llamar al método lock() es diferente del subproceso sincronizado que espera el estado de bloqueo, el método lock() se convertirá en ESPERA; sincronizado se convertirá en BLOQUEADO.
  • Sincronizado es un bloqueo proporcionado por JVM, que bloquea y libera bloqueos automáticamente, mientras que Lock requiere que los desarrolladores bloqueen y liberen los bloqueos por sí mismos.
  • Synchronized puede modificar métodos (métodos estáticos/métodos normales) y bloques de código, mientras que Lock solo puede modificar código.

Supongo que te gusta

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