[Programación concurrente en ocho partes] Las tres características principales de procesos, subprocesos y programación concurrente

Tabla de contenido

¿Cuáles son los conceptos de proceso e hilo?

Un proceso se refiere a un programa en ejecución. Por ejemplo, cuando usa DingTalk o el navegador, debe iniciar este programa y el sistema operativo asignará ciertos recursos a este programa (ocupando recursos de memoria). El subproceso es la unidad básica de programación de la CPU: cada subproceso ejecuta un determinado fragmento del código de un determinado proceso.

¿Los conceptos de serie, paralelo y concurrencia?

La serialización significa hacer cola uno por uno, y solo el segundo puede unirse después de que el primero haya terminado.
Paralelismo significa procesamiento simultáneo.
La concurrencia aquí no es el problema de alta concurrencia de las tres escuelas secundarias, sino el concepto de concurrencia en subprocesos múltiples (el concepto de subprocesos de programación de CPU). La CPU cambia repetidamente para ejecutar diferentes subprocesos en un período de tiempo muy corto, parece ser paralelo, pero es solo un cambio de alta velocidad de la CPU.

El paralelismo abarca la concurrencia.
El paralelismo significa que una CPU de múltiples núcleos programa varios subprocesos al mismo tiempo, lo que en realidad significa que varios subprocesos se ejecutan al mismo tiempo.
Una CPU de un solo núcleo no puede lograr efectos paralelos, pero una CPU de un solo núcleo es concurrencia.

¿Cuáles son los conceptos de sincrónico, asincrónico, bloqueante y no bloqueante?

Síncrono y asíncrono: después de ejecutar una determinada función, si el destinatario enviará información activamente .
Bloqueo y no bloqueo: después de ejecutar una función, ¿la persona que llama debe esperar comentarios sobre el resultado?

Los dos conceptos pueden parecer similares, pero su enfoque es completamente diferente.
Bloqueo sincrónico : por ejemplo, si usa una olla para hervir agua, no se le notificará cuando el agua esté hirviendo. Una vez que comienza a hervir el agua, debe esperar a que hierva.

Sin bloqueo sincrónico : por ejemplo, si usa una olla para hervir agua, no se le notificará cuando el agua esté hirviendo. Una vez que comienza a hervir el agua, no es necesario esperar a que hierva. Puede realizar otras funciones, pero debe comprobar si el agua está hirviendo de vez en cuando.

Bloqueo asincrónico : por ejemplo, si hierve agua con una tetera, después de que hierva, se le notificará que el agua está hervida. Una vez que comienza a hervir el agua, debe esperar a que hierva.

Sin bloqueo asíncrono : Por ejemplo, si hierve agua con un hervidor, después de que hierva, se le notificará que el agua está hervida. Una vez que comienza a hervir el agua, no es necesario esperar a que hierva y podrá realizar otras funciones.

El no bloqueo asincrónico tiene el mejor efecto. Durante el desarrollo normal, la mejor manera de mejorar la eficiencia es utilizar el no bloqueo asincrónico para manejar algunas tareas de subprocesos múltiples.

¿Cómo se crean los hilos?

Heredar la clase Thread y anular el método de ejecución

Para iniciar un hilo, llame al método de inicio, que creará un nuevo hilo y realizará las tareas del hilo. Si llama al método de ejecución directamente, esto hará que el subproceso actual ejecute la lógica empresarial en el método de ejecución.

class Thread implements Runnable {
    
    }
public class MiTest {
    
    
    public static void main(String[] args) {
    
    
        MyJob t1 = new MyJob();
        t1.start();
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("main:" + i);
        }
    }
}
class MyJob extends Thread{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("MyJob:" + i);
        }
    }
}
Implemente la interfaz Runnable y anule el método de ejecución
public class MiTest {
    
    
    public static void main(String[] args) {
    
    
        MyRunnable myRunnable = new MyRunnable();
        Thread t1 = new Thread(myRunnable);
        t1.start();
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("main:" + i);
        }
    }
}
class MyRunnable implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("MyRunnable:" + i);
        }
    }
}
Implemente Callable, reescriba el método de llamada y coopere con FutureTask

Callable se usa generalmente para métodos de ejecución sin bloqueo que devuelven resultados. Sincrónico y sin bloqueo.

public class FutureTask<V> implements RunnableFuture<V> {
    
    }

public interface RunnableFuture<V> extends Runnable, Future<V> {
    
    }
public class MiTest {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        //1. 创建MyCallable
        MyCallable myCallable = new MyCallable();
        //2. 创建FutureTask,传入Callable
        FutureTask futureTask = new FutureTask(myCallable);
        //3. 创建Thread线程
        Thread t1 = new Thread(futureTask);
        //4. 启动线程
        t1.start();
        //5. 做一些操作
        //6. 要结果
        Object count = futureTask.get();
        System.out.println("总和为:" + count);
    }
}
class MyCallable implements Callable{
    
    
    @Override
    public Object call() throws Exception {
    
    
        int count = 0;
        for (int i = 0; i < 100; i++) {
    
    
            count += i;
        }
        return count;
    }
}
Crear subprocesos basados ​​en el grupo de subprocesos

La tarea se envía al grupo de subprocesos y el grupo de subprocesos crea un subproceso de trabajo para ejecutar la tarea.

public class ThreadPoolExecutor extends AbstractExecutorService {
    
    
...
private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
    
    
    ...
}
Clases internas anónimas y enfoque de expresión lambda
// 匿名内部类方式:
  Thread t1 = new Thread(new Runnable() {
    
    
      @Override
      public void run() {
    
    
          for (int i = 0; i < 1000; i++) {
    
    
              System.out.println("匿名内部类:" + i);
          }
      }
  });
  
// lambda方式:
  Thread t2 = new Thread(() -> {
    
    
      for (int i = 0; i < 100; i++) {
    
    
          System.out.println("lambda:" + i);
      }
  });
Resumen: solo hay una forma de alcanzar la capa inferior: implementar Runnble

¿Cuáles son los estados de los hilos?¿Cuáles son los estados de los hilos en Java?

Nivel de sistema operativo: 5 estados (generalmente para estados de subprocesos tradicionales)
Insertar descripción de la imagen aquí
6 estados en Java
Insertar descripción de la imagen aquí

Insertar descripción de la imagen aquí

  • NUEVO : Se crea el objeto Thread, pero el método de inicio aún no se ha ejecutado.

  • RUNNABLE : Cuando el objeto Thread llama al método de inicio, está en el estado RUNNABLE (programación de CPU/sin programación).

  • BLOQUEADO : sincronizado no obtiene el bloqueo de sincronización y está bloqueado.

  • ESPERANDO : Llamar al método de espera lo pondrá en el estado ESPERANDO y deberá activarlo manualmente.

  • TIME_WAITING : llamar al método de suspensión o al método de unión se activará automáticamente, no es necesario activarlo manualmente.

  • TERMINADO : El método de ejecución se ejecuta y el ciclo de vida del subproceso finaliza.

BLOCKED , WAITING , TIME_WAITING : todos pueden entenderse como estados de bloqueo y espera, porque en estos tres estados, la CPU no programará el hilo actual

Ejemplos de 6 estados en Java

//NEW:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
    });
    System.out.println(t1.getState());
}


//RUNNABLE:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while(true){
    
    
        }
    });
    t1.start();
    Thread.sleep(500);
    System.out.println(t1.getState());
}


//BLOCKED:
public static void main(String[] args) throws InterruptedException {
    
    
    Object obj = new Object();
    Thread t1 = new Thread(() -> {
    
    
        // t1线程拿不到锁资源,导致变为BLOCKED状态
        synchronized (obj){
    
    
        }
    });
    // main线程拿到obj的锁资源
    synchronized (obj) {
    
    
        t1.start();
        Thread.sleep(500);
        System.out.println(t1.getState());
    }
}


//WAITING:
public static void main(String[] args) throws InterruptedException {
    
    
    Object obj = new Object();
    Thread t1 = new Thread(() -> {
    
    
        synchronized (obj){
    
    
            try {
    
    
                obj.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    });
    t1.start();
    Thread.sleep(500);
    System.out.println(t1.getState());
}


//TIMED_WAITING:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(500);
    System.out.println(t1.getState());
}


//TERMINATED:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        try {
    
    
            Thread.sleep(500);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(1000);
    System.out.println(t1.getState());
}

¿Cuáles son los métodos comunes de subprocesos, con ejemplos?

Obtener el hilo actual

El método estático de Thread obtiene el objeto de hilo actual.

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
	// 获取当前线程的方法
    Thread main = Thread.currentThread();
    System.out.println(main);
    // "Thread[" + getName() + "," + getPriority() + "," +  group.getName() + "]";
    // Thread[main,5,main]
}
Establecer el nombre del hilo

Después de construir el objeto Thread, asegúrese de establecer un nombre significativo para solucionar errores más adelante.

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        System.out.println(Thread.currentThread().getName());
    });
    t1.setName("模块-功能-计数器");
    t1.start();
}
Establecer prioridad de hilo

De hecho, es la prioridad del subproceso de programación de la CPU. Hay 10 niveles de prioridad establecidos para los subprocesos en Java y cualquier número entero se toma del 1 al 10. Si excede este rango, se eliminarán los errores de excepción de parámetros.

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("t1:" + i);
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("t2:" + i);
        }
    });
    t1.setPriority(1);
    t2.setPriority(10);
    t2.start();
    t1.start();
}
Establecer concesiones para hilos

Puede utilizar el método estático de Thread para cambiar el hilo actual del estado de ejecución al estado listo.

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            if(i == 50){
    
    
                Thread.yield();
            }
            System.out.println("t1:" + i);
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("t2:" + i);
        }
    });
    t2.start();
    t1.start();
}
Establecer hilo de suspensión

El método estático de suspensión de Thread permite que el hilo cambie del estado de ejecución al estado de espera. La suspensión tiene dos sobrecargas de métodos:

  • El primero es modificado por nativo, lo que tiene el efecto de poner el hilo en un estado de espera.
  • El segundo es un método que puede pasar en milisegundos y nanosegundos (si el valor de nanosegundos es mayor o igual a 0,5 milisegundos, agregue 1 al valor de milisegundos inactivo. Si el valor de milisegundos pasado es 0 y el valor de nanosegundos no es 0 , luego duerme durante 1 milisegundo)
// sleep 会抛出一个 InterruptedException
public static void main(String[] args) throws InterruptedException {
    
    
    System.out.println(System.currentTimeMillis());
    Thread.sleep(1000);
    System.out.println(System.currentTimeMillis());
}
Establecer preferencia de hilo

El método de unión del método no estático de Thread debe llamarse en un hilo determinado.

Si se llama a t1.join () en el subproceso principal, el subproceso principal entrará en el estado de espera y deberá esperar a que todos los subprocesos t1 completen la ejecución, y luego regresará al estado listo para esperar la programación de la CPU.

Si se llama a t1.join (2000) en el subproceso principal, el subproceso principal entrará en el estado de espera y deberá esperar a que t1 se ejecute durante 2 segundos antes de volver al estado listo y esperar la programación de la CPU. Si t1 ha finalizado durante el período de espera, el subproceso principal automáticamente queda listo y espera la programación de la CPU.

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            System.out.println("t1:" + i);
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    });
    t1.start();
    for (int i = 0; i < 10; i++) {
    
    
        System.out.println("main:" + i);
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        if (i == 1){
    
    
            try {
    
    
                t1.join(2000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}
Configurar el hilo del demonio

De forma predeterminada, todos los subprocesos son subprocesos que no son demonios y la JVM finalizará la JVM actual cuando no haya subprocesos que no sean demonios en el programa.
El hilo principal es un hilo que no es un demonio de forma predeterminada. Si la ejecución del hilo principal finaliza, debe verificar si hay subprocesos que no son un demonio en la JVM actual. Si no hay una JVM, deténgala directamente.

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            System.out.println("t1:" + i);
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    });
    t1.setDaemon(true);
    t1.start();
}
Establecer hilo de espera y activación

El subproceso que adquiere el recurso de bloqueo sincronizado puede ingresar al grupo de espera de bloqueo a través del método de espera y el recurso de bloqueo se liberará.
El subproceso que adquiere el recurso de bloqueo sincronizado puede activar el subproceso en el grupo de espera y agregarlo al grupo de bloqueo mediante el método notificar o notificar a todos.

notificar activa aleatoriamente un subproceso en el grupo de espera en el grupo de bloqueo.
notifyAll activará todos los subprocesos en el grupo de espera y los agregará al grupo de bloqueo.

Al llamar al método de espera, notificar y norificar a todos los métodos, debe hacerse dentro de un bloque o método de código modificado sincronizado, porque es necesario operar el mantenimiento de la información basado en el bloqueo de un determinado objeto.

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        sync();
    },"t1");

    Thread t2 = new Thread(() -> {
    
    
        sync();
    },"t2");
    t1.start();
    t2.start();
    Thread.sleep(12000);
    synchronized (MiTest.class) {
    
    
        MiTest.class.notifyAll();
    }
}

public static synchronized void sync()  {
    
    
    try {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            if(i == 5) {
    
    
                MiTest.class.wait();
            }
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName());
        }
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
}

¿Cuáles son las formas de finalizar un hilo?

Hay muchas formas de finalizar un hilo. El método más utilizado es finalizar el método de ejecución del hilo, ya sea que finalice mediante retorno o lanzando una excepción.

método de parada (no utilizado)

Forzar el final del hilo, no importa lo que estés haciendo, no es recomendable

    @Deprecated
    public final void stop() {
    
    ...}
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        try {
    
    
            Thread.sleep(5000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(500);
    t1.stop();
    System.out.println(t1.getState());
}
Utilice variables compartidas (rara vez se utilizan)

Este método no se usa mucho y algunos subprocesos pueden seguir ejecutándose en bucles infinitos.
Puede romper el bucle infinito modificando las variables compartidas, dejar que el hilo salga del bucle y finalizar el método de ejecución.

static volatile boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while(flag){
    
    
            // 处理任务
        }
        System.out.println("任务结束");
    });
    t1.start();
    Thread.sleep(500);
    flag = false;
}
modo de interrupción

De forma predeterminada, hay un bit de indicador de interrupción dentro del bit de indicador de interrupción del hilo: falso

Modo de variable compartida

public static void main(String[] args) throws InterruptedException {
    
    
    // 线程默认情况下,    interrupt标记位:false
    System.out.println(Thread.currentThread().isInterrupted());
    // 执行interrupt之后,再次查看打断信息
    Thread.currentThread().interrupt();
    // interrupt标记位:ture
    System.out.println(Thread.currentThread().isInterrupted());
    // 返回当前线程,并归位为 false,interrupt标记位:ture
    System.out.println(Thread.interrupted());
    // 已经归位了
    System.out.println(Thread.interrupted());

    // =====================================================
    Thread t1 = new Thread(() -> {
    
    
        while(!Thread.currentThread().isInterrupted()){
    
    
            // 处理业务
        }
        System.out.println("t1结束");
    });
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
}

Interrumpiendo el hilo en estado WAITING o TIMED_WAITING, lanzando una excepción y manejándolo usted mismo.
Este método de detener hilos es el más utilizado y también es el más común en frameworks y JUC.

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while(true){
    
    
            // 获取任务
            // 拿到任务,执行任务
            // 没有任务了,让线程休眠
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
                System.out.println("基于打断形式结束当前线程");
                return;
            }
        }
    });
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
}

¿Cuál es la diferencia entre esperar y dormir?

  • dormir es un método estático en la clase Thread y esperar es un método en la clase Objeto.
  • dormir pertenece a TIMED_WAITING y se despierta automáticamente. esperar pertenece a WAITING y debe despertarse manualmente.
  • El método de suspensión se ejecuta mientras se mantiene el bloqueo y no liberará los recursos del bloqueo. Después de ejecutar el método de espera, se liberarán los recursos del bloqueo.
  • El modo de suspensión se puede ejecutar mientras se mantiene presionado el candado o no. El método de espera debe ejecutarse solo cuando hay un bloqueo.

El método de espera lanzará el hilo que mantiene el bloqueo de _owner a la colección _WaitSet. Esta operación modifica el objeto ObjectMonitor. Si no se mantiene el bloqueo sincronizado, el objeto ObjectMonitor no se puede operar.

¿Cuáles son las tres características principales de la programación concurrente?

  • atomicidad
  • visibilidad
  • Orden

atomicidad

concepto de atomicidad

JMM (modelo de memoria Java). Diferentes hardware y diferentes sistemas operativos tienen ciertas diferencias en las operaciones de memoria. Para resolver varios problemas que ocurren con el mismo código en diferentes sistemas operativos, Java usa JMM para proteger las diferencias causadas por varios hardware y sistemas operativos.

Haga que la programación concurrente de Java sea multiplataforma.

JMM estipula que todas las variables se almacenarán en la memoria principal . Durante la operación, es necesario copiar una copia de la memoria principal a la memoria del subproceso (memoria de la CPU) para realizar cálculos dentro del subproceso. Luego escríbalo nuevamente en la memoria principal (¡no necesariamente!).

Definición de atomicidad: Atomicidad significa que una operación es indivisible e ininterrumpible. Cuando un hilo se está ejecutando, otro hilo no lo afectará.

La atomicidad de la programación concurrente se ilustra en el código:

private static int count;

public static void increment(){
    
    
    try {
    
    
        Thread.sleep(10);
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
    count++;
}

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
           increment();
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            increment();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

Programa actual: cuando las operaciones multiproceso comparten datos, los resultados esperados no son consistentes con los resultados finales.

Atomicidad: operaciones multiproceso en recursos críticos, los resultados esperados son consistentes con los resultados finales.

A través del análisis de este programa, se puede ver que la operación de ++ se divide en tres partes: primero, el hilo obtiene los datos de la memoria principal y los guarda en el registro de la CPU, luego realiza una operación +1 en el registrar, y finalmente el resultado Escribir de nuevo en la memoria principal.

Garantizar la atomicidad de la programación concurrente.
sincronizado

Porque la operación ++ se puede ver desde las instrucciones.

imagen.png

Puede agregar la palabra clave sincronizada al método o usar un bloque de código sincronizado para garantizar la atomicidad.

sincronizado puede evitar que varios subprocesos operen recursos críticos al mismo tiempo, y solo un subproceso operará recursos críticos al mismo tiempo.

imagen.png

CAS

¿Qué es exactamente CAS?

comparar e intercambiar es comparación e intercambio, que es una primitiva de concurrencia de CPU.

Al reemplazar el valor en una determinada ubicación de la memoria, primero verifica si el valor en la memoria es consistente con el valor esperado y, de ser así, realiza la operación de reemplazo. Esta operación es una operación atómica.

Las clases basadas en inseguridad en Java proporcionan métodos para operar CAS, y la JVM nos ayudará a implementar los métodos en las instrucciones de ensamblaje de CAS.

Pero tenga en cuenta que CAS es solo comparación e intercambio, y debe implementar la operación para obtener el valor original usted mismo.

private static AtomicInteger count = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            count.incrementAndGet();
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            count.incrementAndGet();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

Doug Lea nos ayudó a implementar algunas clases atómicas basadas en CAS, incluido el AtomicInteger que vemos ahora, y muchas otras clases atómicas...

Desventajas de CAS : CAS solo puede garantizar que la operación de una variable sea atómica y no puede lograr atomicidad para múltiples líneas de código.

Preguntas de CAS :

  • Problema ABA : El problema es el siguiente: se puede introducir el número de versión para resolver el problema ABA. Java proporciona una operación para agregar números de versión a cada versión cuando una clase está en CAS. AtomicStampeReferenciaimagen.png
  • Cuando AtomicStampedReference está en CAS, no solo juzgará el valor original, sino que también comparará la información de la versión.
  • public static void main(String[] args) {
          
          
        AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA",1);
    
        String oldValue = reference.getReference();
        int oldVersion = reference.getStamp();
    
        boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
        System.out.println("修改1版本的:" + b);
    
        boolean c = reference.compareAndSet("B", "C", 1, 1 + 1);
        System.out.println("修改2版本的:" + c);
    }
    
  • El problema del tiempo de centrifugado demasiado largo :
    • Puede especificar cuántas veces CAS se repetirá en total. Si excede este número, fallará o colgará directamente el hilo. (Bloqueo de giro, bloqueo de giro adaptativo)
    • Después de que CAS falla una vez, esta operación se puede almacenar temporalmente. Cuando sea necesario obtener los resultados más adelante, se pueden ejecutar todas las operaciones temporales y se pueden devolver los resultados finales.
bloquearbloquear

Lock fue desarrollado por Doug Lea en JDK1.5. Su rendimiento es mucho mejor que el de sincronizado en JDK1.5. Sin embargo, después de la sincronización optimizada de JDK1.6, el rendimiento no es muy diferente, pero si se trata de Cuando hay mucho En caso de concurrencia, se recomienda ReentrantLock y el rendimiento será mejor.

Método para realizar:

private static int count;

private static ReentrantLock lock = new ReentrantLock();

public static void increment()  {
    
    
    lock.lock();
    try {
    
    
        count++;
        try {
    
    
            Thread.sleep(10);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    } finally {
    
    
        lock.unlock();
    }


}

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            increment();
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            increment();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

ReentrantLock se puede comparar directamente con sincronizado y funcionalmente ambos son bloqueos.

Pero ReentrantLock tiene una funcionalidad más rica que sincronizada.

La capa inferior de ReentrantLock se implementa en función de AQS, y hay una variable de estado mantenida en función de CAS para implementar operaciones de bloqueo.

Hilo local

Cuatro tipos de referencia en Java

Los tipos de referencia utilizados en Java son fuerte, suave, débil y virtual .

Usuario usuario = nuevo Usuario();

Lo más común en Java es una referencia fuerte: cuando un objeto se asigna a una variable de referencia, la variable de referencia es una referencia fuerte. Cuando un objeto es referenciado por una variable de referencia fuerte, siempre está en un estado alcanzable y es imposible que el mecanismo de recolección de basura lo recicle. Incluso si el objeto nunca se usará en el futuro, la JVM no lo recolectará. . Por lo tanto, las referencias fuertes son una de las principales causas de las pérdidas de memoria de Java.

SoftReference

En segundo lugar, existen referencias suaves: para los objetos que solo tienen referencias suaves, no se reciclarán cuando la memoria del sistema sea suficiente y se reciclarán cuando el espacio de memoria del sistema sea insuficiente. Las referencias suaves se utilizan a menudo en programas sensibles a la memoria como cachés.

Luego están las referencias débiles, que tienen una vida útil más corta que las referencias suaves. Para objetos con solo referencias débiles, tan pronto como se ejecuta el mecanismo de recolección de basura, independientemente de si el espacio de memoria de la JVM es suficiente, la memoria ocupada por el objeto siempre será regenerado. Puede resolver el problema de las pérdidas de memoria, y ThreadLocal resuelve el problema de las pérdidas de memoria basadas en referencias débiles.

Finalmente, está la referencia virtual, que no se puede usar sola y debe usarse junto con una cola de referencia. La función principal de las referencias virtuales es rastrear el estado de los objetos que se recolectan como basura. Sin embargo, en el desarrollo, todavía utilizamos referencias sólidas con más frecuencia.

La forma en que ThreadLocal garantiza la atomicidad es evitar que varios subprocesos operen recursos críticos y permitir que cada subproceso opere sus propios datos.

Código

static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();

public static void main(String[] args) {
    
    
    tl1.set("123");
    tl2.set("456");
    Thread t1 = new Thread(() -> {
    
    
        System.out.println("t1:" + tl1.get());
        System.out.println("t1:" + tl2.get());
    });
    t1.start();

    System.out.println("main:" + tl1.get());
    System.out.println("main:" + tl2.get());
}

Principio de implementación de ThreadLocal:

  • Cada Thread almacena una variable miembro, ThreadLocalMap
  • ThreadLocal en sí no almacena datos, es como una clase de herramienta que opera ThreadLocalMap basada en ThreadLocal.
  • ThreadLocalMap en sí se implementa en función de Entry [], porque un hilo puede vincular varios ThreadLocals, por lo que es posible que sea necesario almacenar varios datos, por lo que se implementa en forma de Entry [].
  • Cada ThreadLocalMap existente tiene su propio ThreadLocalMap independiente y luego utiliza el objeto ThreadLocal como clave para acceder al valor.
  • La clave de ThreadLocalMap es una referencia débil. La característica de la referencia débil es que incluso si hay una referencia débil, debe reciclarse durante la GC. Esto es para evitar que el objeto ThreadLocal se recicle si la referencia a la clave es una referencia fuerte después de que el objeto ThreadLocal pierde su referencia.

Problema de pérdida de memoria de ThreadLocal:

  • Si se pierde la referencia de ThreadLocal, el GC reciclará la clave debido a la referencia débil. Si el hilo no se ha reciclado al mismo tiempo, provocará una pérdida de memoria y el valor en la memoria no se puede reciclar y no se puede obtener.
  • Solo necesita llamar al método remove a tiempo para eliminar la Entrada después de usar el objeto ThreadLocal.

imagen.png

visibilidad

concepto de visibilidad

El problema de visibilidad ocurre según la ubicación de la CPU. La velocidad de procesamiento de la CPU es muy rápida. En comparación con la CPU, es demasiado lento para obtener datos de la memoria principal. La CPU proporciona cachés de tres niveles de L1, L2 y L3. Cada vez que va a la memoria principal, después de que los datos se recuperan de la memoria, se almacenarán en el caché de nivel 3 de la CPU. Cada vez que los datos se recuperan del caché de nivel 3, la eficiencia definitivamente mejorará .

Esto ha traído problemas. Hoy en día las CPU son multinúcleo y la memoria de trabajo (caché de tercer nivel de la CPU) de cada hilo es independiente. Al realizar modificaciones, a cada hilo se le notificará que solo se modificará su propia memoria de trabajo. y no habrá una respuesta oportuna Sincronizado con la memoria principal, lo que provoca problemas de inconsistencia en los datos.

imagen.png

Lógica de código para problemas de visibilidad

private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            // ....
        }
        System.out.println("t1线程结束");
    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
Formas de resolver la visibilidad
volátil

Volátil es una palabra clave utilizada para modificar variables miembro.

Si un atributo se modifica de forma volátil, equivale a decirle a la CPU que la operación del atributo actual no puede usar el caché de la CPU y debe operar con la memoria principal.

Semántica de memoria de volátil:

  • Se escribe el atributo volátil: al escribir una variable volátil, JMM actualizará rápidamente el caché de la CPU correspondiente al subproceso actual en la memoria principal.
  • Se lee el atributo volátil: al leer una variable volátil, JMM establecerá la memoria correspondiente en el caché de la CPU como no válida y la variable compartida debe volver a leerse desde la memoria principal.

De hecho, agregar volátil es informar a la CPU que las operaciones de lectura y escritura del atributo actual no pueden usar el caché de la CPU. El atributo modificado con volátil se agregará con un prefijo de bloqueo después de convertirse en ensamblador. Cuando la CPU ejecuta esta instrucción, si le pone el prefijo lock hará dos cosas:

  • Escribe los datos de la línea de caché del procesador actual en la memoria principal.
  • Los datos reescritos no son directamente válidos en el caché de otros núcleos de CPU.

Resumen: Volátil significa que cada vez que la CPU opera con estos datos, debe sincronizarse inmediatamente con la memoria principal y leer los datos de la memoria principal.

private volatile static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            // ....
        }
        System.out.println("t1线程结束");
    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
sincronizado

Sincronizado también puede resolver problemas de visibilidad y semántica de memoria sincronizada.

Si se trata de un bloque de código de sincronización sincronizado o un método de sincronización, después de adquirir el recurso de bloqueo, las variables internas involucradas se eliminarán de la memoria caché de la CPU y los datos deben recuperarse de la memoria principal. Después de liberar el bloqueo, la CPU Los datos en caché se sincronizarán inmediatamente con la memoria principal.

private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            synchronized (MiTest.class){
    
    
                //...
            }
            System.out.println(111);
        }
        System.out.println("t1线程结束");

    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
Cerrar

La forma en que Lock garantiza la visibilidad es completamente diferente de la sincronizada: según su semántica de memoria, sincronizada sincroniza el caché de la CPU con la memoria principal al adquirir y liberar bloqueos.

El bloqueo se implementa en función de lo volátil. Al bloquear y liberar el candado dentro del candado Lock, se agregará o restará un atributo de estado modificado por volátil.

Si se realiza una operación de escritura en un atributo modificado volátil, la CPU ejecutará la instrucción con el prefijo de bloqueo y la CPU sincronizará inmediatamente los datos modificados desde el caché de la CPU a la memoria principal, y también sincronizará inmediatamente otros atributos con la memoria principal. . Estos datos en otras líneas de caché de la CPU también se configurarán como no válidos y deberán extraerse de la memoria principal nuevamente.

private static boolean flag = true;
private static Lock lock = new ReentrantLock();

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            lock.lock();
            try{
    
    
                //...
            }finally {
    
    
                lock.unlock();
            }
        }
        System.out.println("t1线程结束");

    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
final

Los atributos modificados finalmente no pueden modificarse durante el tiempo de ejecución. De esta manera, se garantiza indirectamente la visibilidad. Todos los subprocesos múltiples que leen los atributos finales deben tener el mismo valor.

Final no significa que cada vez que se recuperan datos se leen de la memoria principal, no es necesario y final y volátil no pueden modificar un atributo al mismo tiempo.

El contenido modificado por final ya no puede volver a escribirse, y volátil garantiza que cada lectura y escritura de datos se lea desde la memoria principal, y volátil afectará cierto rendimiento, por lo que no es necesario modificarlo al mismo tiempo.

imagen.png

Orden

concepto de orden

En Java, el contenido del archivo .java se compilará y deberá convertirse nuevamente en instrucciones que la CPU pueda reconocer antes de la ejecución. Cuando la CPU ejecuta estas instrucciones, para mejorar la eficiencia de la ejecución sin afectar el resultado final (satisfaciendo algunos requisitos), las instrucciones se reorganizarán.

La razón por la cual las instrucciones se ejecutan desordenadamente es para maximizar el rendimiento de la CPU.

Los programas en Java se ejecutan desordenadamente.

El programa Java verifica el efecto de la ejecución desordenada:

static int a,b,x,y;

public static void main(String[] args) throws InterruptedException {
    
    
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
    
    
        a = 0;
        b = 0;
        x = 0;
        y = 0;

        Thread t1 = new Thread(() -> {
    
    
            a = 1;
            x = b;
        });
        Thread t2 = new Thread(() -> {
    
    
            b = 1;
            y = a;
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        if(x == 0 && y == 0){
    
    
            System.out.println("第" + i + "次,x = "+ x + ",y = " + y);
        }
    }
}

El modo Singleton puede tener problemas debido al reordenamiento de las instrucciones:

Los subprocesos pueden obtener objetos no inicializados, lo que puede causar algunos problemas innecesarios al usarlos porque las propiedades internas tienen valores predeterminados.

private static volatile MiTest test;

private MiTest(){
    
    }

public static MiTest getInstance(){
    
    
    // B
    if(test  == null){
    
    
        synchronized (MiTest.class){
    
    

            if(test == null){
    
    
                // A   ,  开辟空间,test指向地址,初始化
                test = new MiTest();
            }
        }
    }
    return test;
}
como en serie

semántica como si fuera en serie:

No importa cómo se especifique el reordenamiento, es necesario asegurarse de que el resultado de la ejecución del programa de un solo subproceso permanezca sin cambios.

Y si hay dependencias, las instrucciones no se pueden reorganizar.

// 这种情况肯定不能做指令重排序
int i = 0;
i++;

// 这种情况肯定不能做指令重排序
int j = 200;
j * 100;
j + 100;
// 这里即便出现了指令重排,也不可以影响最终的结果,20100
sucede antes

Reglas específicas:
  1. Principio de suceder antes de un solo subproceso: en el mismo hilo, escriba la operación anterior antes de la operación posterior.
  2. El principio de suceder antes de las cerraduras: la operación de desbloqueo de la misma cerradura ocurre antes de la operación de bloqueo de esta cerradura.
  3. El principio de ocurre antes de volátil: una operación de escritura en una variable volátil ocurre antes de cualquier operación en esta variable.
  4. El principio de transitividad de suceder-antes: si A opera suceder-antes de B, y B sucede-antes de C, entonces A sucede-antes de C.
  5. El principio de suceder antes del inicio del subproceso: el método de inicio del mismo subproceso ocurre antes que otros métodos de este subproceso.
  6. El principio de suceder antes de la interrupción del subproceso: la llamada al método de interrupción del subproceso ocurre antes de que el subproceso interrumpido detecte el código enviado por la interrupción.
  7. El principio de suceder antes de la terminación del hilo: todas las operaciones en un hilo se detectan antes de la terminación del hilo.
  8. El principio de creación de objetos de suceder antes: la inicialización de un objeto se completa antes de que se llame a su método de finalización.
JMM no activará el efecto de reorganización de instrucciones solo cuando no ocurran las 8 condiciones anteriores.

No es necesario que preste demasiada atención al principio de sucede antes, solo necesita poder escribir código seguro para subprocesos.

volátil

Si necesita evitar que el programa reorganice las instrucciones cuando opera un determinado atributo, además de cumplir con el principio de sucede antes, también puede modificar el atributo en función de volátil, de modo que no ocurra el problema de la reorganización de las instrucciones. al operar este atributo.

¿Cómo prohíbe volátil la reorganización de instrucciones?

Concepto de barrera de memoria. Piense en una barrera de memoria como una instrucción.

Se agregará una instrucción previa entre las dos operaciones, esta instrucción puede evitar el reordenamiento de otras instrucciones ejecutadas hacia arriba y hacia abajo.

Supongo que te gusta

Origin blog.csdn.net/qq_44033208/article/details/132434143
Recomendado
Clasificación