Para conocer los subprocesos múltiples de Java, ¡este artículo es suficiente! (Súper detallado)

Tabla de contenido

1. Comprender el hilo

1. Concepto

 2. El primer programa multiproceso.

 (1) Observar hilos

 3. Crea un hilo

2. Clase de hilo y métodos comunes.

1. Métodos de construcción comunes de Thread

2. Varios atributos comunes de Thread

 3. Iniciar un hilo: iniciar

4. Terminar un hilo

(1) El programador establece la bandera manualmente.

(2) Clase de hilo directo

 5. Espera un hilo

6. Obtenga la referencia del hilo actual.

7. Dormir el hilo actual

3. Estado del hilo

1. Observe todos los estados de los hilos. 

NUEVO

EJECUTABLE

OBSTRUIDO 

ESPERA 

TIMED_WAITING

TERMINADO

2. El significado del estado del hilo y la transferencia de estado.

4. Riesgos que conlleva el subproceso múltiple: seguridad del subproceso (el más importante) 

1. Demostración de problemas de seguridad de subprocesos 

2. Causas de los problemas de seguridad de los subprocesos

 3. Resuelva el problema de inseguridad del hilo anterior.

Cuarto, palabra clave sincronizada

5. Problemas causados ​​por la visibilidad de la memoria.

6. espera y notifica

 7. Comparación entre esperar y dormir

5. Caso de subprocesos múltiples

1. Modo singleton

(1) Modo Hombre Hambriento

(2) Modo perezoso

(3) Resuelva el problema de seguridad de subprocesos del modo diferido

(4) Problema de visibilidad de la memoria en modo diferido

2. Cola de bloqueo

3. Temporizador

4. Grupo de subprocesos

6. Estrategias de bloqueo comunes

1. Bloqueo optimista versus bloqueo pesimista

2. Candado pesado versus candado liviano

3. Bloqueo de giro VS suspender bloqueo de espera

4. Bloqueo de lectura-escritura VS bloqueo mutex

5. Bloqueo justo VS bloqueo injusto

6. Bloqueo reentrante VS bloqueo no reentrante

7. Punto muerto

8. Principio sincronizado

1. Funciones básicas 

2. Proceso de bloqueo 

 9. CAS

1. Concepto 

2. Aplicación de CAS

(1) Implementar la clase atómica

(2) Implementar bloqueo de giro

 3. Cuestiones ABA del CAS

10. Clases comunes de JUC (java.util.concurrent)

1. Interfaz colaborable

2、Bloqueo reentrante

3. clase atómica

4. Semáforo

5、CountDownLatch

11. Clase de colección

(1) Usar ArrayList en múltiples entornos

(2) Entorno multiproceso que utiliza colas

(3) El entorno multiproceso utiliza una tabla hash

(a) tabla hash

(b) Mapa concurretnhash


1. Comprender los hilos (Thread )

1. Concepto

El multiproceso ya ha logrado muy bien el efecto de la programación concurrente.

Pero hay una desventaja obvia: el proceso es demasiado pesado.

1. Consumir más recursos

2. Velocidad más lenta

Está bien si los procesos se crean y destruyen con poca frecuencia. Una vez que los procesos deben crearse y destruirse a gran escala, la sobrecarga será relativamente alta.

Los gastos generales se reflejan principalmente en el proceso de asignación de recursos a los procesos.

Entonces, el programador inteligente pensó en una manera: al crear un proceso, ¿puede solo asignar una PCB simple en lugar de asignar memoria y recursos del disco duro posteriores?

Entonces hay un proceso liviano, es decir, un hilo (Thread)

El hilo solo crea una PCB, pero no asigna recursos posteriores como memoria, disco duro, etc.

Los subprocesos se crean para realizar algunas tareas, pero realizar tareas requiere consumir estos recursos de hardware.

Entonces, lo que creamos sigue siendo un proceso, pero cuando creamos un proceso, todos los recursos se asignan y los subprocesos creados posteriormente se mantienen dentro del proceso ( la relación entre procesos e subprocesos puede considerarse como procesos que contienen subprocesos ).

Los nuevos subprocesos en procesos posteriores reutilizan directamente los recursos creados aquí en el proceso anterior.

De hecho, un proceso debe contener al menos un hilo.

Por lo tanto, el creado inicialmente puede considerarse como un proceso que contiene solo un subproceso (el proceso de creación en este momento necesita asignar recursos y la sobrecarga de creación del primer subproceso puede ser relativamente grande en este momento)

Pero si crea un hilo en este proceso más adelante, puede omitir el proceso de asignación de recursos, ya que los recursos ya están allí.

Varios subprocesos en un proceso reutilizan conjuntamente varios recursos (memoria, disco duro) en el proceso, pero estos subprocesos se programan de forma independiente en la CPU.

Por lo tanto, los subprocesos no solo pueden lograr el efecto de "programación concurrente", sino que también lo realizan de una manera relativamente liviana.

Los hilos también se describen a través de PCB.

En Windows, los procesos y subprocesos se describen mediante estructuras diferentes, pero en Linux, los desarrolladores de Linux reutilizan la estructura de PCB para describir subprocesos.

En este momento, una PCB corresponde a un hilo y varias PCB corresponden a un proceso.

El puntero de memoria y la tabla de descriptores de archivos en la PCB. En múltiples PCB del mismo proceso, el contenido de estos dos campos es el mismo, pero el estado del contexto, la prioridad, la información contable... admiten atributos de programación. Cada una de estas PCB es diferente

Entonces están estas dos oraciones:

El proceso es la unidad básica de asignación por parte del sistema operativo.

Los subprocesos son la unidad básica de programación y ejecución del sistema operativo.

En comparación con la programación multiproceso, la programación multiproceso tiene ventajas: es más liviana y más rápida de crear y destruir.

Pero también tiene desventajas: no es tan estable como el proceso

En Java, al realizar programación concurrente, aún debe considerar el subproceso múltiple

Ya sea "multiproceso" o "multiproceso", son esencialmente modelos de implementación de "programación concurrente", de hecho, existen muchos otros modelos de implementación de "programación concurrente". 

Hablar sobre las diferencias y conexiones entre procesos y subprocesos.

1. Los procesos incluyen subprocesos, todo para lograr "programación concurrente", los subprocesos son más livianos que los procesos.

2. El proceso es la unidad básica de asignación de recursos por parte del sistema, y ​​el subproceso es la unidad básica de programación y ejecución del sistema. Al crear un proceso, el trabajo de asignación de recursos se realiza. Si crea un subproceso más adelante, puede compartir directamente los recursos anteriores.

3. Los procesos tienen espacios de direcciones independientes y no se afectan entre sí. La independencia de los procesos mejora la estabilidad del sistema. Varios subprocesos comparten un espacio de direcciones. Una vez que un subproceso lanza una excepción, puede provocar que todo el proceso finalice de forma anormal, lo que Significa que varios subprocesos pueden afectarse fácilmente entre sí. 

4. Hay algunos otros puntos, pero los tres puntos centrales mencionados anteriormente son muy importantes.

Los subprocesos son más livianos, pero no están exentos de costos de creación. Si los subprocesos se crean/destruyen con mucha frecuencia, no se puede ignorar la sobrecarga.

En ese momento, el programador inteligente pensó en dos formas más;

1. "Hilo ligero", es decir, corrutina/fibra

Esto aún no está integrado en la biblioteca estándar de Java, pero existen algunas bibliotecas de terceros que implementan corrutinas.

2. "Grupo de subprocesos"

Pool (encuesta) es en realidad una forma muy clásica de pensar en las computadoras: no libere algunos recursos para que se liberen rápidamente, sino colóquelos en un pool para su uso posterior; cuando solicite recursos, también debe solicitarlos en avance Es fácil solicitar recursos y ponerlos en un "grupo", lo cual es más conveniente para solicitudes posteriores.


 2. El primer programa multiproceso.

Los subprocesos en sí son un concepto proporcionado por el sistema operativo, y el sistema operativo también proporciona algunas API para que las utilicen los programadores (Linux, pthread).

En Java, la API del sistema operativo está encapsulada y se proporciona la clase de subproceso.

Primero cree una clase, déjela heredar el hilo y luego reescriba el método de ejecución. La ejecución aquí es equivalente al método de entrada del hilo. Una vez que el hilo comienza a ejecutarse, esta ejecución describe qué hacer.

No solo necesita crear una clase, también debe llamarla y dejar que se ejecute. El comienzo aquí es crear un hilo (esta operación llamará a la API proporcionada por el sistema operativo en la parte inferior y al mismo tiempo creará un hilo en el kernel del sistema operativo. Estructura de PCB correspondiente y agregada a la lista vinculada correspondiente)

En este momento, el subproceso recién creado participará en la programación de la CPU . El siguiente trabajo que realizará este subproceso es el método de ejecución que acabamos de reescribir anteriormente. 

java.lang es un paquete especial. Las clases aquí no necesitan importarse manualmente y pueden usarse de forma predeterminada.

class Mythread extends Thread{
    @Override
    public void run() {
        System.out.println("hello world!");
    }
}


public class Demo1 {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        mythread.start();
    }
}

Es decir, cada vez que haga clic para ejecutar un programa, primero se creará un proceso Java, este proceso contiene al menos un hilo, este hilo también se llama hilo principal, que es el hilo responsable de ejecutar el método principal.

Si cambiamos el código anterior a mythread.run();, encontraremos que también se puede ejecutar correctamente, pero es diferente desde el inicio.

run es solo el método de entrada anterior (método ordinario), no llama a la API del sistema y no crea un hilo real.

Ahora ajustamos el código para que se vea así:

class Mythread extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread!");
        }
    }
}


public class Demo1 {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        mythread.start();
        //mythread.run();

        while(true){
            System.out.println("hello main!");
        }
    }
}

En este momento, hay un bucle while en el método principal y también hay un bucle while en la ejecución del hilo. Ambos bucles son bucles while (ture) infinitos.

Utilice el método de inicio para ejecutar. En este momento, ambos bucles se están ejecutando e imprimiendo resultados alternativamente. Esto se debe a que los dos subprocesos ejecutan sus propios bucles respectivamente. Ambos subprocesos pueden participar en la programación de la CPU. Estos dos subprocesos se ejecutan simultáneamente.

La relación entre el hilo principal y la ejecución simultánea del estilo de hilo nuevo depende de cómo la programe el sistema operativo.

Si está escrito en modo de ejecución, significa que no se ha creado ningún subproceso nuevo y todavía está dentro del subproceso principal original. Solo después de que se ejecuta el tiempo en ejecución, puede regresar a la ubicación de llamada y ejecutarse más tarde. while in run Es un bucle infinito, por lo que while en el hilo principal no tiene posibilidad de ejecutarse

Cada hilo es un flujo de ejecución independiente.

Cada hilo puede ejecutar un fragmento de código.

Existe una relación de ejecución concurrente entre cada hilo. 

A veces, agregaremos un sueño para ejecutar. 

Thread.sleep(1000);

Hemos unificado estas diferencias en Java. Thread.sleep es equivalente a encapsular las funciones del sistema anteriores. Si se ejecuta en la versión de Windows de jvm, la capa inferior es llamar a Sleep de Windows. Si se ejecuta en la versión de Linux de jvm, la capa inferior es para llamar a Linux en suspensión

dormir es un método estático de la clase Thread

Para esta excepción, actualmente elegimos usar el método try-catch para resolverlo, luego, si lo ejecutamos nuevamente, se convertirá en una ejecución rítmica.


Nota: Esta no es una alternancia muy estricta y el orden también puede invertirse.

Esto se debe a que los dos subprocesos entrarán en el estado de bloqueo después de dormir. Cada vez que se acabe el tiempo, el sistema despertará los dos subprocesos y reanudará la programación de los dos subprocesos. Cuando ambos subprocesos se despierten, se puede considerar el orden de programación. como "aleatorio"

Cuando el sistema programa varios subprocesos, no tiene un orden muy claro, pero los programa de esta manera "aleatoria", este proceso de programación "aleatorio" se denomina "ejecución preventiva". 

La aleatoriedad de la que hablamos habitualmente en realidad implica establecer una "igual probabilidad", pero la aleatoriedad aquí al menos parece aleatoria, pero en realidad no garantiza necesariamente un perfil igual.


 (1) Observar hilos

Después de crear un hilo, también podemos observarlo directamente de alguna manera. 

1. Utilice el depurador de ideas directamente

Al ejecutar, elija ejecutar de acuerdo con el método de depuración.

 

2、jconsola

Herramientas de depuración oficiales proporcionadas en jdk.

Podemos encontrar jconsole en el directorio bin de acuerdo con la ruta de descarga de jdk anterior.

Luego ejecute el programa primero y luego abra jconsole, que enumera todos los proyectos Java que se ejecutan en el sistema (jconsole también es un programa escrito en Java)

 Luego seleccione el hilo que se conecta y luego seleccione la página de etiqueta del hilo

En este momento, todos los subprocesos se muestran en la esquina inferior izquierda. Aquí se enumeran todos los subprocesos del proceso actual, no solo el subproceso principal y los subprocesos creados por usted mismo, sino también algunos otros subprocesos. Los subprocesos restantes están todos en la JVM. Viene con él y es responsable de completar algunos otros aspectos del trabajo.

Después de hacer clic en el hilo, puede ver la información detallada del hilo.

Seguimiento de pila: pila de llamadas de Thread, que muestra la relación de llamada entre métodos

Cuando el programa se atasca, podemos verificar la pila de llamadas de cada hilo aquí y podemos saber aproximadamente en qué código ocurrió la situación atascada.


 3. Crea un hilo

 En Java, hay muchas formas de crear subprocesos mediante la clase Thread.

1. Cree una clase, herede Thread y anule el método de ejecución

2. Cree una clase, implemente Runnable y anule el método de ejecución.

La implementación se refiere a implementar la interfaz. 

En Java, la interfaz generalmente se describe utilizando una palabra adjetiva parte del discurso.

package thread;

import java.util.TreeMap;

class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

//使用 Runnable 的方式创建线程
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
        t.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

la diferencia:

Thread Aquí, el trabajo a completar se coloca directamente en el método de ejecución de Thread.

Aquí Runnable está separado, el trabajo a completar se coloca en Runnable y luego se permite que Runnable y Thread cooperen.

Desacopla aún más las tareas que debe realizar el hilo del hilo mismo.

3. Heredar hilo, reescribir y ejecutar, según una clase interna anónima

1. Primero crea una subclase. Esta subclase hereda de Thread, pero esta subclase no tiene nombre (anónima), por otro lado, esta clase fue creada en Demo3.

2. En la subclase, el método de ejecución se reescribe nuevamente.

3. Cree una instancia de la subclase y use la referencia t para señalar

//通过匿名内部类的方式,创建线程

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run() {
                while(true){
                    System.out.println("hello Thead!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };

        t.start();
        while(true){
            System.out.println("hello main!");
            Thread.sleep(1000);
        }

    }
}

4. Implemente Runnable, reescriba la ejecución, basada en una clase interna anónima

1. Creó una subclase Runnable (clase, implementa Runnable)

2. Reescrito el método de ejecución.

3. Cree una instancia de la subclase y pásela al constructor de Thread. 

) corresponde al final del constructor Thread. Toda la nueva sección Runnable está construyendo un parámetro Thread.

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello Thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });

        t.start();
        while(true){
            System.out.println("hello main!");
            Thread.sleep(1000);
        }
    }
}

5. Utilice expresiones lambda para expresar el contenido de la ejecución (recomendado)

 Una expresión lambda es esencialmente una "función anónima"

Estas funciones anónimas se pueden utilizar principalmente como funciones de devolución de llamada.

La función de devolución de llamada no requiere que los programadores la llamemos activamente, sino que se llama automáticamente en el momento adecuado.

Escenarios donde se utilizan a menudo "funciones de devolución de llamada":

1. Desarrollo del servidor: el servidor recibe una solicitud y activa la función de devolución de llamada correspondiente.

2. Desarrollo de interfaz gráfica: una determinada operación del usuario desencadena una devolución de llamada correspondiente

La función de devolución de llamada aquí en realidad se ejecuta después de que el hilo se crea correctamente.

Al igual que lambda, en esencia no hay nuevas características del lenguaje, pero las funciones que se pueden implementar en el pasado están escritas de una manera más concisa. 

public class Demo5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(true){
                System.out.println("hello Thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t.start();

    }
}

No solo existen los cinco métodos anteriores para crear subprocesos, sino también métodos basados ​​en Callable y thread pool , que son los métodos de escritura más comunes en la actualidad.


2. Clase de hilo y métodos comunes.

Thread es el portavoz de los subprocesos de Java, es decir, un subproceso en el sistema corresponde a un objeto Thread en Java . A través de Thread se realizan varias operaciones en torno a subprocesos. 

1. Métodos de construcción comunes de Thread

Java nombra subprocesos: Thread-0, que es menos legible. Si hay docenas de subprocesos en un programa con diferentes funciones, entonces podemos nombrar los subprocesos al crearlos. 

public class Demo6 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(true){
                System.out.println("hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"myThread");

        t.start();

    }
}

 Aquí llamamos al hilo "Mythread". Cuando se ejecuta el código, puede encontrar el hilo en jconsole.

Descubriremos que en este momento no hay ningún main en el hilo, esto se debe a que main ha terminado de ejecutarse. 

El subproceso se crea mediante el inicio y se ejecuta un método de entrada de subproceso (para el subproceso principal, es principal; para otros subprocesos, ejecuta / lambda)


2. Varios atributos comunes de Thread

ID: obtenerId()

La identidad del hilo (la identidad establecida para el hilo en la JVM)

Una persona puede tener muchos nombres y un hilo también puede tener varias identidades.

La JVM tiene una identidad, la biblioteca pthread (la API del sistema operativo proporcionada por el sistema a los programadores) y una identidad. En el kernel, el PCB para subprocesos también tiene una identidad. Estas identidades son independientes entre sí .

Estado, prioridad:

Existen ciertas diferencias entre el estado del hilo en Java y el sistema operativo.

Establecer/obtener la prioridad no es muy útil, porque el núcleo del sistema es el principal responsable de la programación de subprocesos.

Ya sea hilo de fondo:

Hilo de fondo/hilo de demonio: el hilo de fondo no afecta el final

Hilo de primer plano: Afectará el final del proceso, si el hilo de primer plano no ha terminado de ejecutarse, el proceso no finalizará.

Si todos los subprocesos en primer plano de un proceso han terminado de ejecutarse, incluso si los subprocesos en segundo plano no han terminado de ejecutarse, saldrán junto con el proceso.

El hilo creado es un hilo en primer plano de forma predeterminada. Puede configurar el proceso en segundo plano a través de setDeamon ().

//后台线程 和 前台线程

public class Demo7 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(true){
                System.out.println("hello Thread!");

            }
    });

        //设置成后台线程
        t.setDaemon(true);
        t.start();

    }
}

¿Está vivo?

Indica si el hilo correspondiente al objeto Thread (en el kernel del sistema) está vivo

En otras palabras, ¡ el ciclo de vida del objeto Thread no es completamente consistente con los subprocesos del sistema! !

Generalmente, el objeto Thread se crea primero y se llama manualmente a start antes de que el kernel realmente cree el hilo.

Cuando muere, puede ser que el objeto Thread finalice su ciclo de vida primero (no se hace referencia a este objeto)

También es posible que el objeto Thread todavía esté allí y que el hilo en el kernel termine de ejecutarse primero y luego finalice.


 3. Iniciar un hilo: iniciar

En el sistema, cree un hilo:

1. Crea la PCB

2. Agregue la PCB a la lista vinculada correspondiente

Esto lo hace el núcleo del sistema. 

¿Qué es el kernel del sistema operativo?

Sistema operativo = kernel + programas de soporte

El kernel contiene las funciones principales de un sistema:

1. Correcto, administra varios dispositivos de hardware.

2. Correcto, proporciona un entorno operativo estable para varios programadores.

Lo más crítico es: llamar a la API del sistema para completar el trabajo de creación del hilo.

La ejecución del método de inicio en sí se completa en un instante. Después de llamar a inicio, el código continuará inmediatamente ejecutando la lógica de inicio posterior.


4. Terminar un hilo

Un hilo finaliza después de ejecutar el método de ejecución.

La terminación aquí es encontrar una manera de completar la ejecución lo antes posible.

En circunstancias normales, no habrá una situación en la que el hilo desaparezca repentinamente antes de que se complete la ejecución. Si un hilo completa la mitad del trabajo y desaparece repentinamente, a veces causará algunos problemas.

(1) El programador establece la bandera manualmente.

A través de esta bandera configurada manualmente

//线程终止

public class Demo8 {
    public static boolean isQuit = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while(!isQuit){
                System.out.println("hello Thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t.start();

        //主线程这里执行一些其它逻辑之后,让 t 线程结束
        Thread.sleep(3000);

        //这个代码就是在修改前面设置的标志位
        isQuit = true;
        System.out.println("把线程终止");
    }
}

A través del código anterior, el hilo t finaliza y el sueño (3000) en realidad se imprime 4 veces, lo cual es normal.

La razón principal es que hay errores en la operación de suspensión. Una vez que el tiempo de suspensión real es superior a 3000, el hilo t puede imprimir el cuarto registro, por lo que es posible imprimir 3 o 4 veces.

¿Por qué se puede acceder a isQuit cuando se escribe como una variable miembro, pero se informará un error cuando se escribe como una variable local?

 La expresión lambda puede capturar variables externas

Dado que el tiempo de ejecución de la expresión lambda es posterior, esto generará la posibilidad de que la variable local isQuit haya sido destruida cuando la lambda realmente se ejecute más tarde.

En otras palabras, el hilo principal termina primero y isQuit se destruye en este momento. Cuando lambda realmente se ejecuta, isQuit se destruye.

Esta situación existe objetivamente: obviamente es inapropiado permitir que lambda acceda a una variable que no ha sido destruida.

lambda introduce un mecanismo como "captura de variables"

Parece que lambda accede directamente a las variables externas internamente, de hecho, esencialmente está copiando las variables externas en la lambda (esto puede resolver el problema del ciclo de vida en este momento)

Sin embargo, la captura de variables tiene una limitación.

La captura de variables requiere que la variable capturada sea final 

Cuando eliminamos isQuit = true;, no se producirá ningún error, pero se mostrará un mensaje:

Esto significa que aunque no se utiliza la modificación final, la variable no se modifica en el código, en este momento la variable también se puede considerar como final. 

Si esta variable desea modificarse, la captura de variables no se puede realizar en este momento.

Entonces, ¿por qué necesitamos configurar esto?

Java implementa "captura de variable" copiando . Si el código externo quiere modificar esta variable, ocurrirá una situación: la variable externa ha cambiado, pero la variable interna no ha cambiado (es probable que ocurra ambigüedad)

Por el contrario, otros lenguajes (JS) adoptan un diseño más radical: también existe la captura de variables, que no se implementa mediante copia, sino que cambia directamente el ciclo de vida de las variables externas, asegurando así que lambda pueda acceder definitivamente al exterior durante la ejecución. .variables (en este momento, la captura de variables en js no tiene límite final)

Si escribe variables miembro, no es un mecanismo que activa la "captura de variables", sino "la clase interna accede a miembros de la clase externa", lo cual está bien en sí mismo y no necesita preocuparse por si es final.


(2) Clase de hilo directo

La clase Thread nos proporciona indicadores listos para usar, por lo que no necesitamos configurarlos manualmente.

//线程终止,使用 Thread 自带的标志位

public class Demo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            //Thread.currentThread() 其实就是 t
            //但是 lambda 表达式是在构造 t 之前就定义好了的,编译器看到的 lambda 里的 t 就会认为这是一个还没初始化的对象
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello Thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t.start();
        Thread.sleep(3000);
        
        //把上述的标志位设置成 true
        t.interrupt();
    }
}

Cuando se ejecuta el código, encontraremos que el programa no se ejecuta según nuestras ideas.

Según el mensaje, podemos entenderlo como: el hilo está inactivo y luego se despierta mediante una interrupción.

Si el bit de bandera se configura manualmente, no se puede despertar el modo de suspensión en este momento. 

Un hilo puede estar ejecutándose normalmente o inactivo. Si este hilo está inactivo, ¿debería despertarse?

Todavía necesito despertar

Por lo tanto, cuando un subproceso está inactivo y otros subprocesos llaman al método de interrupción, el modo de suspensión se verá obligado a generar una excepción y el modo de suspensión se despertará inmediatamente ( suponiendo que haya configurado la suspensión (1000), aunque solo han pasado 10 ms, no 1000 ms). , también se despertará inmediatamente)

¡Pero cuando se despierta el sueño, se borrará automáticamente la bandera establecida previamente! ! !

En este punto, si desea continuar dejando que el hilo termine, simplemente agregue una pausa al cierre.

//线程终止,使用 Thread 自带的标志位

public class Demo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            //Thread.currentThread() 其实就是 t
            //但是 lambda 表达式是在构造 t 之前就定义好了的,编译器看到的 lambda 里的 t 就会认为这是一个还没初始化的对象
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello Thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //throw new RuntimeException(e);
                    break;
                }
            }
        });

        t.start();
        Thread.sleep(3000);

        //把上述的标志位设置成 true
        t.interrupt();
    }
}

Una vez que se despierta el sueño, los programadores pueden proceder de las siguientes maneras:

1. Detenga el bucle inmediatamente y finalice el hilo inmediatamente.

2. Continúe haciendo otra cosa y luego finalice el hilo después de un tiempo (ejecute otra lógica en catch y luego interrumpa después de la ejecución)

3. Ignore la solicitud terminada y continúe el bucle (no escriba pausa)

 

El primero es un método estático y la bandera de juicio se borrará al mismo tiempo que se realiza el juicio.

Este último es un método miembro, por lo que se recomienda utilizar este último. 


 5. Espera un hilo

Se ejecutan varios subprocesos al mismo tiempo y el sistema operativo programa el proceso de ejecución específico.

El proceso de programación del sistema operativo es "aleatorio"

Por lo tanto, no podemos determinar el orden en que se ejecutan los subprocesos.

Esperar subprocesos es una forma de planificar el orden en que terminan los subprocesos.

Esperar el final es esperar a que el método de ejecución complete la ejecución.

Bloqueo: permita que el código no continúe ejecutándose temporalmente (el hilo no participará en la programación en la CPU por el momento)

Los hilos también se pueden bloquear mediante el modo de suspensión, pero este bloqueo tiene un límite de tiempo.

El bloqueo de la unión está "esperando la muerte".

¿Se puede activar la unión mediante una interrupción?

Sí, dormir, unirse, esperar ... Después del bloqueo, es posible que el método de interrupción lo despierte, estos métodos borrarán automáticamente el bit de bandera después de despertar (similar a dormir)

public class Demo10 {
    public static void main(String[] args) {
        Thread b = new Thread(() -> {
            for(int i = 0;i < 5;i ++){
                System.out.println("hello B!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("b 结束了");
        });
        
        Thread a = new Thread(() -> {
            for(int i = 0;i < 3;i ++){
                System.out.println("hello A!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            try {
                //如果 b 此时还没执行完毕, b.join 就会产生阻塞情况
                b.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("A 结束了");
        });

        b.start();
        a.start();

    }
}

6. Obtenga la referencia del hilo actual.

Estamos muy familiarizados con este método.

public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
   }
}

7. Dormir el hilo actual

También es un grupo de métodos con los que estamos más familiarizados. Una cosa para recordar es que debido a que la programación de subprocesos no es controlable, este método solo puede garantizar que el tiempo de sueño real sea mayor o igual al tiempo de sueño establecido por los parámetros.

El parámetro es ms como unidad. 

Pero el sueño en sí tiene ciertos errores.

Configure el modo de suspensión (1000), ¡no necesariamente significa dormir exactamente durante 1000 ms! ! (La programación de subprocesos también lleva tiempo)

dormir (1000) significa que el hilo volverá al "estado listo" después de 1000 ms. En este momento, se puede ejecutar en la CPU en cualquier momento, pero es posible que no se especifique de inmediato.

Por las características del sueño nació una técnica especial: dormir(0)

Deje que el hilo actual renuncie a la CPU y equipe la siguiente ronda de programación (generalmente no involucrada en el desarrollo en segundo plano)

El efecto del método de rendimiento es el mismo que el del sueño (0) 

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
   }
}

3. Estado del hilo

1. Observe todos los estados de los hilos. 

 Java proporciona un total de los siguientes seis estados:

NUEVO

Se ha creado el objeto Thread, pero aún no se ha llamado al método Thread.

Esto se puede verificar con el código: 

//线程的状态

public class Demo11 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
        while(true){
           // System.out.println("hello Thread!");
        }
        });

        System.out.println(t.getState());

        t.start();

    }
}

EJECUTABLE

 El estado listo puede entenderse como dos situaciones:

(1) El hilo se está ejecutando en la CPU.

(2) Los subprocesos se ponen en cola aquí y se pueden ejecutar en la CPU en cualquier momento.

//线程的状态

public class Demo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            while(true){

            }
        });

        System.out.println(t.getState());

        t.start();

        //t.join();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

OBSTRUIDO 

Porque la cerradura crea un bloqueo.

ESPERA 

El bloqueo se produce porque se llama a esperar .

TIMED_WAITING

Porque  el sueño bloquea 

Si utiliza la versión para unirse con tiempo de espera, también se generará TIMED_WAITING.

//线程的状态

public class Demo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            while(true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        System.out.println(t.getState());

        t.start();

        //t.join();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

TERMINADO

estado del trabajo completado 

//线程的状态

public class Demo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        System.out.println(t.getState());

        t.start();

        t.join();
        System.out.println(t.getState());
    }
}

2. El significado del estado del hilo y la transferencia de estado.

 Si utiliza una forma más sencilla y fácil de entender para comprender la transición entre estados, puede utilizar la siguiente figura para representarla:


4. Riesgos que conlleva el subproceso múltiple: seguridad del subproceso (el más importante) 

1. Demostración de problemas de seguridad de subprocesos 

Primero veamos este código y sus resultados:

//线程安全问题演示

class Counter{
    public  int count = 0;

    public void increase(){
        count++;
    }
}

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

        Thread t1 = new Thread(() ->{
            for(int i = 0;i < 50000;i++){
                counter.increase();
            }
        });

        Thread t2 = new Thread(() ->{
            for(int i = 0;i < 50000;i++){
                counter.increase();
            }
        });

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

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

        System.out.println(counter.count);
    }
}

Lógicamente hablando, el resultado de la ejecución en este momento debería ser 100000, pero aparecieron los siguientes resultados:

Lógicamente hablando, dos subprocesos realizan un autoincremento cíclico para una variable, cada uno incrementándola 50.000 veces. El resultado esperado debería ser 10.000.000, pero en realidad no lo es. Y cuando ejecutamos el programa varias veces, encontraremos que los resultados de Cada operación es diferente. No es lo mismo. 

En subprocesos múltiples, descubrimos que los errores causados ​​​​por la ejecución de subprocesos múltiples se denominan colectivamente "problemas de seguridad de subprocesos". Si un determinado código no tiene problemas para ejecutarse en uno o varios subprocesos, se denomina "seguro para subprocesos". lo contrario se llama "hilo inseguro"

Entonces, si los subprocesos son seguros depende de si hay errores en el código, especialmente errores causados ​​por subprocesos múltiples. 

count++;

Esta operación consta esencialmente de tres pasos:

1. Cargue los datos de la memoria en el registro de la CPU (cargar).

2. Agregue +1 a los datos del registro (agregar)

3. Escriba los datos del registro en la memoria (sava).

Si las operaciones anteriores las realizan dos o más subprocesos al mismo tiempo, pueden ocurrir problemas.

Para representar mejor el proceso de ejecución de este conteo, trazamos una línea de tiempo para comprender mejor

Dado que aquí el orden de programación de estos dos subprocesos es incierto, también habrá diferencias en el orden relativo de estos dos conjuntos de operaciones.

                                                                                                                       

Aunque se incrementa dos veces, dado que los dos subprocesos se ejecutan al mismo tiempo, los resultados intermedios de la operación pueden sobrescribirse en un determinado orden de ejecución.

Durante estos 50.000 ciclos, ¿cuántas veces son seriales los ++ de los dos subprocesos y cuántas veces aparecerá el resultado de sobrescritura? No es seguro porque la programación del subproceso es "aleatoria" .

Aquí, el resultado aquí causará el problema i, y el valor de error obtenido debe ser inferior a 10w

Muchos códigos implican problemas de seguridad de subprocesos, no solo de recuento++


2. Causas de los problemas de seguridad de los subprocesos

1. [Causa raíz] El orden de programación entre varios subprocesos es "aleatorio" y el sistema operativo utiliza una estrategia de ejecución "preventiva" para programar los subprocesos.

A diferencia del subproceso único, en el subproceso múltiple el orden de ejecución del código ha producido más cambios, en el pasado solo era necesario considerar que el código se ejecutaba en un orden fijo y se ejecutaba correctamente. Ahora debemos considerar que en subprocesos múltiples, bajo N órdenes de ejecución, los resultados de la ejecución del código deben ser correctos.

Todos los principales sistemas operativos actuales utilizan este tipo de ejecución preventiva.

2. Varios subprocesos modifican la misma variable al mismo tiempo, lo que fácilmente puede causar problemas de seguridad de subprocesos.

Un hilo modifica una variable, varios hilos leen la misma variable y varios hilos modifican varias variables. Las tres situaciones están bien.

Esta condición en realidad habla del problema de la estructura del código . Podemos evitar esta situación ajustando la estructura del código. Esta es también la forma más importante de resolver el problema de seguridad de los subprocesos.

3. Las modificaciones realizadas no son "atómicas" 

Si la operación de modificación se puede completar de manera atómica, no habrá problemas de seguridad de subprocesos en este momento.

4. Problemas de seguridad de subprocesos causados ​​por la visibilidad de la memoria

5. Problemas de seguridad de subprocesos causados ​​por el reordenamiento de instrucciones 


 3. Resuelva el problema de inseguridad del hilo anterior.

 El bloqueo equivale a empaquetar un conjunto de operaciones en una operación "atómica".

La operación atómica de la transacción se basa principalmente en la reversión, y la operación atómica aquí es "mutuamente excluyente" a través de bloqueos: cuando mi hilo está funcionando, otros hilos no pueden funcionar.

El bloqueo en el código permite que varios subprocesos utilicen esta variable al mismo tiempo.

Frente al código anterior, la operación de bloqueo se realiza cuando se realiza el conteo ++.

En Java, se introduce una palabra clave sincronizada. 

Cuando bloqueamos un método, significa que al ingresar al método, quedará bloqueado (locked),   al salir del método, se desbloqueará (unlocked). 

Después de que t1 se bloquea, t2 también intenta bloquear. En este momento, t2 se bloqueará y esperará. Este bloqueo continuará hasta que t1 libere el bloqueo y t2 pueda bloquear con éxito. 

La espera de bloqueo de t2 aquí pospone la operación ++ de t2 para contar hasta más tarde. Solo cuando t1 completa el recuento ++, t2 puede realizar el recuento ++, convirtiendo la " ejecución intercalada" en "ejecución en serie".

Entonces, en este momento, echemos un vistazo al efecto después del bloqueo:

A través de la operación de bloqueo, la ejecución concurrente se convierte en ejecución en serie, entonces, ¿el subproceso múltiple todavía tiene significado en este momento?

Nuestro hilo actual no solo cuenta++. El método de aumento se vuelve serial debido al bloqueo. Sin embargo, el bucle for anterior no está bloqueado porque no implica problemas de seguridad del hilo. La variable que operé en el bucle for es una variable local en la pila. Dos subprocesos tienen dos espacios de pila independientes, que son variables completamente diferentes.

En otras palabras, i en los dos subprocesos no es la misma variable. Si los dos subprocesos modifican dos variables diferentes, no hay problema de seguridad del subproceso y no es necesario bloquear.

Por lo tanto, para estos dos subprocesos, parte del código se ejecuta en serie y parte se ejecuta al mismo tiempo, lo que sigue siendo más eficiente que la ejecución en serie pura.


Cuarto, palabra clave sincronizada

Esta palabra clave es el método de bloqueo (palabra clave) proporcionado por Java.

Esto suele hacerse con bloques de código:

1. Bloquear al ingresar al bloque de código

2. Desbloquear después de salir del bloque de código.

El bloqueo y desbloqueo sincronizados en realidad se amplía con la dimensión del "objeto".

El propósito del bloqueo es utilizar recursos para exclusión mutua (variables de modificación mutuamente excluyentes)

Cuando se usa sincronizado, en realidad se especifica un objeto específico para bloquear. Cuando sincronizado modifica directamente el método, es equivalente a bloquear esto (el método modificado es equivalente a la simplificación del código anterior)

Si dos subprocesos bloquean el mismo objeto, se producirá competencia de bloqueo/conflicto de bloqueo (un subproceso puede bloquearse con éxito y el otro subproceso se bloquea y espera)

Si dos subprocesos bloquean objetos diferentes , no habrá competencia de bloqueo, no habrá bloqueo, espera ni una serie de operaciones.

Al igual que el código de ahora, si dos subprocesos bloquean objetos diferentes, no habrá espera de bloqueo y los dos subprocesos no podrán realizar count++ en serie, y aún habrá seguridad para los subprocesos.

Por ejemplo, el siguiente código: 

//线程安全问题演示

class Counter{
    public  int count = 0;
    public Object locker = new Object();

     public void increase(){
        synchronized (this){
            count++;
        }
    }

    public void increase2(){
         synchronized (locker){
             count++;
         }
    }
}

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

        Thread t1 = new Thread(() ->{
            for(int i = 0;i < 50000;i++){
                counter.increase();
            }
        });

        Thread t2 = new Thread(() ->{
            for(int i = 0;i < 50000;i++){
                counter.increase2();
            }
        });

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

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

        System.out.println(counter.count);
    }
}

Pero si cambia el objeto de bloqueo del primer aumento a casillero, seguirá siendo el mismo objeto en este momento y se producirá una espera de bloqueo, lo que naturalmente resolverá el problema de seguridad del subproceso.

No importa qué objeto esté bloqueado; lo más importante es si los dos subprocesos bloquean el mismo objeto.

Si en el siguiente código, un hilo está bloqueado y el otro no está bloqueado, ¿habrá un problema de seguridad del hilo en este momento? 

¡Todavía hay un problema! ! !

Una bofetada en la cara no puede emitir ningún sonido, el bloqueo unilateral significa que no hay bloqueo. ¡Solo tiene sentido tener varios hilos bloqueando el mismo objeto!

No importa qué objeto bloquees. No tienes que escribir esto en (). No importa lo que escribas. Lo que importa es si las operaciones de bloqueo de múltiples subprocesos son para el mismo objeto. El objeto escrito en sincronizado puede ser uno, pero de hecho el miembro de la operación puede ser otro

Si usa sincronizado para modificar un método estático, es equivalente a usar sincronizado para bloquear el objeto de clase.

La información completa de una clase está inicialmente en el archivo .java y luego se compilará en una .class. Cuando jvm carga la .class, analizará el contenido y construirá un objeto en la memoria, que es el objeto de clase de la clase. .

En sincronizado, no hay diferencia si escribe un objeto de clase o un objeto ordinario en (). A sincronizado no le importa cuál es el objeto, solo le importa si los dos subprocesos bloquean el mismo objeto.


5. Problemas causados ​​por la visibilidad de la memoria.

Ahora hay este fragmento de código: 

 t1 siempre realiza un bucle while y t2 permite al usuario ingresar un número entero a través de la consola como valor de isQuit.

Cuando la entrada del usuario sigue siendo 0, el hilo t1 continúa ejecutándose. Cuando la entrada del usuario no es 0, el hilo debe finalizar el ciclo.

//内存可见性

import java.util.Scanner;

public class Demo13 {

    public static int isQuit = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() ->{
            while(isQuit == 0){

            }
            System.out.println("t1 执行结束");
        });

        Thread t2 = new Thread(() ->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入 isQuit 的值");
            isQuit = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

 Pero cuando ejecutamos el programa, encontramos un problema: cuando ingresamos un valor distinto de 0, el valor de isQuit se modificó, pero el subproceso t1 aún se está ejecutando.

Cuando el programa se compila y ejecuta, el compilador de Java y jvm pueden realizar algunas "optimizaciones" en el código.

La optimización del compilador esencialmente se basa en el código para analizar, juzgar y ajustar de manera inteligente el código que usted escribe. En la mayoría de los casos, este ajuste es correcto y puede garantizar que la lógica permanezca sin cambios. Sin embargo, si encuentra subprocesos múltiples, estos errores pueden ocurrir durante optimización, lo que hace que la lógica original en el programa cambie.

Este juicio condicional esencialmente da dos instrucciones:

1. cargar (leer memoria), la velocidad de operación de lectura de la memoria es muy lenta

2. jcmp (comparar y saltar), esta comparación es una operación de registro y es extremadamente rápida

En este momento, el compilador/JVM descubrió que en esta lógica, el código necesita leer el mismo valor de memoria repetida y rápidamente, y el valor de esta memoria sigue siendo el mismo cada vez que se lee, por lo que el compilador pone en negrita decisión Decisión: optimice directamente la operación de carga, ya no cargue en el futuro, compare directamente los datos en el registro 

Sin embargo, no esperaba que el programador modificara el valor de isQuit en otro hilo. En este momento, el compilador no pudo determinar con precisión si se ejecutaría el hilo t2 y cuándo, por lo que se produjo un error de juicio.

Aunque el valor de isQuit en la memoria se cambia aquí, el valor de isQuit no se lee repetidamente en otro hilo, por lo que el hilo t1 no puede detectar la modificación de t2 y ocurre el problema anterior.

La palabra clave volátil se utiliza para compensar los defectos anteriores.

Después de usar volátil para modificar una variable, el compilador entenderá que esta variable es "volátil" y no puede optimizar la operación de lectura en el registro de la manera anterior (el compilador deshabilitará la optimización anterior), por lo que puede garantizar que t1 pueda Siempre lea los datos en la memoria durante el ciclo.

Podemos ver que después de agregar volátiles al código fuente, el hilo t1 puede terminar normalmente.

Volátil esencialmente garantiza la visibilidad de la memoria de la variable (prohibiendo que la operación de lectura de la variable se optimice en el registro de lectura) 

La optimización del compilador es en realidad una cuestión metafísica: cuándo optimizar y cuándo no optimizar, no podemos entender las reglas.

Si el código anterior se modifica ligeramente, es posible que la optimización anterior no se active, por ejemplo:

Cuando agregamos suspensión, la suspensión afectará en gran medida la velocidad del ciclo while. Si la velocidad es lenta, el compilador no continuará optimizándose. En este momento, incluso si no se agrega volátil, los cambios de memoria se pueden detectar a tiempo. Entiendo

Suplemento: la memoria de trabajo (memoria de trabajo) no es la memoria de la arquitectura von Neumann de la que estamos hablando, sino el registro de la CPU + el caché de la CPU, denominados colectivamente "memoria de trabajo".

Problemas de visibilidad de la memoria:

1. Optimización del compilador

2. Modelo de memoria

3. Subprocesos múltiples

¡volátil garantiza visibilidad de la memoria, no atomicidad! ! !


6. espera y notifica

esperar y notificar también son herramientas importantes en subprocesos múltiples

La programación de subprocesos múltiples es "aleatoria" y muchas veces esperamos que se puedan ejecutar varios subprocesos en el orden que especificamos para completar la cooperación entre subprocesos.

esperar y notificar son herramientas importantes para coordinar el orden de los hilos.

Estos dos métodos son métodos proporcionados por Object, es decir, puedes usar esperar y notificar para cualquier objeto.

Ahora, cuando intentamos utilizar el método de espera, el compilador solicita:

Significado: cuando la espera hace que un subproceso se bloquee, puede utilizar el método de interrupción para reactivar el subproceso e interrumpir el estado de bloqueo del subproceso actual.

Luego lanzamos la excepción, ejecutamos el código nuevamente y descubrimos que ocurre una excepción:

Significado: excepción de estado de bloqueo ilegal

El estado de bloqueo no es más que dos: bloqueado y desbloqueado.

Entonces, ¿por qué ocurre un error?

Cuando se vuelva a ejecutar la espera, hará tres cosas:

1. Desbloquear, object.wait intentará desbloquear el objeto objeto

2. Bloqueo y espera

3. Cuando otros subprocesos lo despierten, intentará volver a bloquearlo. Si el bloqueo tiene éxito, se ejecutará la espera y se continuará ejecutando otra lógica.

El requisito previo para esperar a desbloquear es bloquearlo primero, pero ni siquiera lo hemos bloqueado todavía. 

Idea central: bloquear primero y luego esperar sincronizado

Cuando bloqueemos este código y luego ejecutemos el programa, encontraremos que ha ingresado exitosamente al estado de bloqueo y la espera aquí se bloqueará hasta que otros subprocesos notifiquen.

El objetivo principal de esperar y notificar es organizar el orden de ejecución entre subprocesos. Uno de los escenarios más típicos es evitar eficazmente el "hambre/inanición de subprocesos".

Demostración de esperar y notificar:

//wait 和 notify
public class Demo15 {
    //使用这个锁对象来负责加锁,wait,notify
    private static Object locker = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() ->{
            while(true){
                synchronized (locker){
                    System.out.println("t1 wait 开始");
                    try {
                        locker.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("t1 wait 结束");
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(()->{
            while(true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker){
                    System.out.println("t2 notify 开始");
                    locker.notify();
                    System.out.println("t2 notify 结束");
                }
            }
        });
        t2.start();

    }
}

Efecto de ejecución:

Algunas notas:

1. Si desea notificar para despertar la espera sin problemas, debe asegurarse de que esperar y notificar se llamen utilizando el mismo objeto.

2. Tanto la espera como la notificación deben colocarse en sincronización. Aunque la notificación no implica una "operación de desbloqueo", Java requiere que la notificación se coloque en sincronización.

3. Si otro hilo no está en estado de espera cuando se realiza la notificación, notificar equivale a "disparar en vano" y no tendrá ningún efecto secundario.

Puede haber i subprocesos. Por ejemplo, puede haber N subprocesos en espera y un subproceso es responsable de notificar. En este momento, la operación de notificación solo activará un subproceso. El subproceso que se activa es aleatorio.

notifyAll puede activar todos los hilos en espera

Si desea despertar un hilo específico, puede dejar que diferentes hilos usen diferentes objetos para esperar. Si desea despertar a alguien, puede usar el objeto correspondiente para notificar


 7. Comparación entre esperar y dormir

El sueño tiene una hora clara. Se despertará naturalmente cuando llegue la hora. También puede despertarse con anticipación. Simplemente use la interrupción.

De forma predeterminada, la espera está en un estado de espera muerta, esperando hasta que otros subprocesos notifiquen, y la espera también se puede activar por adelantado mediante una interrupción.

notificar puede entenderse como un despertar suave: después del despertar, el hilo debe continuar funcionando y luego entrará en el estado de espera.

La interrupción le dice al hilo que está a punto de terminar, y luego el hilo ingresará al trabajo final.

esperar también tiene una versión con tiempo de espera (similar a unirse)

Por lo tanto, coordine el orden de ejecución entre varios subprocesos y, por supuesto, dé prioridad al uso de esperar y notificar en lugar de dormir.


5. Caso de subprocesos múltiples

1. Modo singleton

El patrón singleton es un patrón de diseño.

Los patrones de diseño son el juego de ajedrez del programador, que presenta muchos escenarios típicos y cómo lidiar con escenarios típicos.

Escenarios correspondientes al modo singleton: a veces, esperamos que algunos objetos tengan solo una instancia (objeto) en todo el programa, lo que significa que solo pueden ser nuevos una vez.

No es confiable confiar en las personas como garantía. Aquí debemos confiar en el compilador para realizar una verificación más estricta y encontrar una manera de hacer que el código solo nuevo el objeto una vez. Si se intenta nuevo varias veces, se informará un error directamente.

En Java, hay muchas formas de lograr este efecto, aquí presentamos principalmente dos :

1. Se inicia el programa en modo hambriento (urgente) y se crea una instancia inmediatamente después de cargar la clase.

2. El modo diferido (retraso) crea una instancia cuando se usa por primera vez; de lo contrario, no se creará si es posible.

Por ejemplo:

El compilador abre un archivo. Supongamos que hay un archivo muy grande. Algunos compiladores cargarán todo el contenido en la memoria a la vez (hombre hambriento) , y algunos compiladores solo cargarán parte del contenido, y las otras partes serán paginadas por el usuario. Cuando llegue el momento, cárgalo nuevamente cuando lo necesites (tipo vago)

(1) Modo Hombre Hambriento

Con estático, representa atributos de clase. Dado que el objeto de clase de cada clase es un singleton, los atributos del objeto de clase (estático) también son singleton.

Cuando hacemos una restricción en el código: prohibimos a otros crear esta instancia nueva, es decir, cambiamos el método de construcción a privado

La ejecución real de este código es cuando la jvm carga la clase Singleton, que se cargará cuando la jvm se use por primera vez, no necesariamente cuando se inicie el programa.                                                         

 En este momento, este código se compila e informa un error.

En este momento , sólo hay una . Al escribir de esta manera, puede implementar efectivamente el modo singleton.

Entonces, si el constructor está configurado como privado, ¿definitivamente no se podrá llamar desde afuera?

Al utilizar la reflexión, puede crear varias instancias en el modo singleton actual. Sin embargo, la reflexión en sí misma es un método de programación "no convencional". Durante el desarrollo normal, debe usarse con precaución. El abuso de la reflexión traerá grandes riesgos: hará que el código sea más abstracto y difícil de mantener.

//单例模式(饿汉模式)
class Singleton{
    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return  instance;
    }

    //做出一个限制:禁止别人去 new 这个实例
    private Singleton(){

    }
}

public class Demo16 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        //Singleton s3 = new Singleton();  会报错

        System.out.println(s1 == s2);
        //System.out.println(s1 == s3);
    }
}

(2) Modo perezoso

class Singletonlazy{
    private static Singletonlazy instance = null;

    public static Singletonlazy getInstance(){
        if (instance == null){
            instance = new Singletonlazy();
        }
        return instance;
    }

    private Singletonlazy(){

    }
}
//单例模式(懒汉模式)

public class Demo17 {
    public static void main(String[] args) {
        Singletonlazy s1 = Singletonlazy.getInstance();
        Singletonlazy s2 = Singletonlazy.getInstance();
        System.out.println(s1 == s2);
    }
}

 El frente es todo un presagio, vayamos al grano:

Entre los dos modos que acabamos de mencionar, cuál es seguro para subprocesos, es decir, cuando varios subprocesos llaman a getInstance, qué código es seguro para subprocesos (no habrá errores)

Para el modo hombre hambriento : en subprocesos múltiples, no hay problema en leer el contenido de la instancia y leer la misma variable en múltiples subprocesos.

Para modo diferido : aquí es donde surgen los problemas

En este momento, SingletonLazy será nuevo y creará dos objetos , y ya no será un singleton.

En este momento, habrá preguntas: ¿La segunda nueva operación no modifica la referencia de la instancia original? ¿No se recicló inmediatamente el objeto que antes era nuevo?, ¿no queda al final todavía un objeto?

Nota: La sobrecarga de renovar un objeto puede ser muy grande. Si lo renueva varias veces, le costará una gran sobrecarga.

Conclusión: ¡el código en modo diferido no es seguro para subprocesos! ! !


(3) Resuelva el problema de seguridad de subprocesos del modo diferido

Podemos solucionar este problema bloqueando, por lo que tenemos el siguiente código:

Sin embargo, esta forma de escribir es incorrecta y  no resuelve el problema de seguridad de subprocesos anterior . Esto se debe a que la operación en sí es atómica y bloquearla nuevamente no genera ningún cambio sustancial.

Entonces, ¿cómo modificarlo?

Agrega la cerradura al exterior;

    public static Singletonlazy getInstance(){
        synchronized (Singletonlazy.class){
            if (instance == null){
                instance = new Singletonlazy();
            }
        }
        return instance;
    }

De esta forma se solucionan los problemas anteriores, pero aún quedan otros problemas:

El bloqueo es una operación relativamente costosa y el bloqueo puede provocar una espera de bloqueo.

El principio básico del bloqueo debe ser: no bloquear a menos que sea necesario y no bloquear sin pensar. Si el bloqueo se realiza sin pensar, la eficiencia de ejecución del programa se verá afectada. 

Esta forma de escribir significa que cada llamada posterior a GetInstance debe bloquearse, pero no es necesario .

El hilo en modo diferido no es seguro. El problema ocurre principalmente cuando el objeto es nuevo por primera vez. Una vez que el objeto es nuevo, no habrá ningún problema si se llama a getInstance más tarde.

En otras palabras, una vez que un objeto es nuevo, las condiciones posteriores no pueden ingresar y no habrá operaciones de modificación involucradas, solo operaciones de lectura.

De esta forma, el bloqueo se realiza tanto en la primera llamada como en las siguientes, de hecho, las llamadas posteriores no necesitan bloquearse, aquí se bloquean los lugares que no deben bloquearse, lo que afectará en gran medida la eficiencia de la programa.

Entonces, ¿cómo solucionar tal problema?

Primero determine si desea bloquear y luego decida si realmente bloqueará.

    public static Singletonlazy getInstance(){
        // instance 如果为空,就说明是首次调用,需要加锁,如果是非null,就说明是后续调用,不用加锁
        if (instance == null){
            synchronized (Singletonlazy.class){
                if (instance == null){
                    instance = new Singletonlazy();
                }
            }
        }

        return instance;
    }

¿Es razonable escribir la misma condición si dos veces?

¡Muy razonable! ! !

Esto se debe a que la operación de bloqueo puede bloquearse y es imposible determinar durante cuánto tiempo estará bloqueada. Puede haber un intervalo de tiempo muy largo entre la segunda condición y la primera condición. Durante este largo intervalo de tiempo, otros subprocesos pueden cambiar la instancia.

La primera condición: determinar si bloquear

Segunda condición: determinar si se debe crear un objeto


(4) Problema de visibilidad de la memoria en modo diferido

Supongamos que hay dos subprocesos ejecutándose al mismo tiempo. Después de modificar la instancia, el primer subproceso finaliza el bloque de código y libera el bloqueo. El segundo subproceso puede adquirir el bloqueo y recuperarse del bloqueo. Luego, el segundo subproceso realizará una operación de lectura . realizado por dos subprocesos ¿podrá leer el valor modificado por el primer subproceso? 

Este es el problema de visibilidad de la memoria. 

Aquí, necesitamos agregar un volátil a la instancia. 

Si habrá un problema de visibilidad de la memoria aquí, solo hemos analizado que puede existir tal riesgo, pero no está claro si el compilador realmente activará la optimización en este escenario.

En realidad, esto es fundamentalmente diferente del código anterior: la visibilidad de la memoria anterior era leída repetidamente por un hilo, pero ahora es leída repetidamente por varios hilos. No se sabe si la optimización anterior se activará en este momento, pero agregar volátil es más seguro práctica

Agregar volátil aquí tiene otro propósito: evitar el reordenamiento de instrucciones para operaciones de asignación aquí

El reordenamiento de instrucciones también es un medio de optimización del compilador: al tiempo que se garantiza que la lógica de ejecución original permanezca sin cambios, el orden de ejecución del código se ajusta para mejorar la eficiencia de ejecución después del ajuste.

Si se trata de un solo subproceso, dicho reordenamiento suele estar bien. Si se trata de varios subprocesos, pueden surgir problemas.

Esta operación implica los siguientes tres pasos: 

1. Cree un espacio de memoria para el objeto y obtenga la dirección de memoria.

2. Llame al método constructor en el espacio para inicializar el objeto.

3. Asigne la dirección de memoria a la referencia de instancia.

Esto puede implicar reordenamiento de instrucciones , y 123 puede convertirse en 132. Si se trata de un solo subproceso, no importa si se ejecuta primero 2 o 3 en este momento, pero no es necesariamente el caso de subprocesos múltiples. 

O supongamos que hay dos subprocesos ejecutándose al mismo tiempo.

Supongamos que el primer subproceso se ejecuta en el orden de 1 3 2, y después de ejecutar 3 y antes de 2, se produce un cambio de subproceso. En este momento, antes de que haya tiempo para inicializar el objeto, se programa para otros subprocesos.

¡ A continuación, el segundo hilo se ejecuta y determina la instancia! = nulo, por lo que la instancia se devuelve directamente y algunos atributos o métodos de la instancia se pueden usar más adelante, pero el objeto obtenido aquí es un objeto incompleto y no inicializado . Utilice atributos/métodos para este objeto incompleto. Pueden surgir algunas situaciones.

Después de agregar volátil a la instancia, la operación de asignación realizada en la instancia no provocará el reordenamiento de las instrucciones anteriores y debe ejecutarse en el orden 123 en lugar de 132.

class Singletonlazy{
    private static volatile Singletonlazy instance = null;

    public static Singletonlazy getInstance(){
        // instance 如果为空,就说明是首次调用,需要加锁,如果是非null,就说明是后续调用,不用加锁
        if (instance == null){
            synchronized (Singletonlazy.class){
                if (instance == null){
                    instance = new Singletonlazy();
                }
            }
        }

        return instance;
    }

    private Singletonlazy(){

    }
}

2. Cola de bloqueo

La cola de bloqueo tiene función de bloqueo:

1. Cuando la cola está llena, si continúa ingresando a la cola, se producirá el bloqueo hasta que otros subprocesos tomen elementos de la cola.

2. Cuando la cola está vacía, si continúa quitando la cola, se producirá el bloqueo hasta que otros subprocesos agreguen elementos a la cola.

Las colas de bloqueo son muy útiles: en el desarrollo back-end, basado en colas de bloqueo, se puede implementar el modelo productor-consumidor.

El modelo productor-consumidor es una forma de abordar problemas de subprocesos múltiples, por ejemplo:

Hay tres pasos para hacer bolas de masa: 1. Amasar la masa 2. Extender los envoltorios de las bolas de masa 3. Hacer bolas de masa

Durante el Año Nuevo chino, cuando la familia prepara bolas de masa juntas, existen dos estrategias:

1. Cada persona es responsable de extender las pieles de las bolas de masa, y envolver cada una después de extenderlas es relativamente ineficiente.

2. Una persona es responsable de extender los envoltorios de las bolas de masa y las otras dos personas son responsables de hacer las bolas de masa [modelo productor-consumidor].

Productor: la persona que extiende los envoltorios de las bolas de masa. Consumidor: la persona que hace las bolas de masa. Lugar de comercio: el lugar donde se colocan los envoltorios de las bolas de masa.

Ventajas del modelo productor-consumidor :

1. Desacoplamiento

Desacoplar significa “reducir el acoplamiento entre módulos”

Considere un sistema distribuido:

Cuando solo hay A y B en la sala de computadoras, A envía directamente la solicitud a B, y el acoplamiento entre A y B es más obvio.

(1) Si B muere, puede tener un gran impacto en B. Si A muere, también tendrá un gran impacto en B.

(2) Suponiendo que se agrega un servidor C en este momento, es necesario realizar cambios importantes en el código de A.

En este momento, si se introduce el modelo productor-consumidor y se introduce una cola de bloqueo, los problemas anteriores se pueden resolver de manera efectiva.

En este momento, A y B están bien desacoplados a través de la cola de bloqueo.

En este momento, si A o B cuelgan, no tendrá mucho impacto ya que no interactúan directamente entre sí.

Si desea agregar un nuevo servidor C, el servidor A no necesita ninguna modificación en este momento, simplemente deje que C también tome elementos de la cola.

2. Afeitado de picos y relleno de valles

Las solicitudes que el servidor recibe de los clientes/usuarios no son estáticas y el número de solicitudes puede aumentar considerablemente debido a algunas emergencias.

Existe un límite superior en la cantidad de solicitudes que un servidor puede manejar al mismo tiempo, y diferentes servidores tienen diferentes límites superiores.

En un sistema distribuido, a menudo sucede que algunos pueden soportar una mayor presión, mientras que otros pueden soportar menos presión.

En este momento, cada vez que A recibe una solicitud, B debe procesar una solicitud de inmediato.

Si A puede soportar una presión mayor y B puede soportar una presión menor, B puede morir primero.

Pero si se utiliza el modelo de consumo del productor, la historia es diferente.

Cuando las solicitudes externas aumentan repentinamente y A recibe más solicitudes, A escribirá más datos de solicitudes en la cola, pero B aún puede procesar las solicitudes de acuerdo con el ritmo original y no colgará.

Es equivalente a que la cola actúe como un amortiguador: la cola absorbe la presión que originalmente estaba en B (corte de pico)

El pico a menudo es solo temporal: cuando el pico disminuye, A recibe menos solicitudes, B aún las maneja de acuerdo con el ritmo establecido y B no estará demasiado inactivo ( llenando el valle).

Debido a que el modelo productor-consumidor es tan importante, aunque la cola de bloqueo es solo una estructura de datos, implementaremos esta estructura de datos por separado en un programa de servidor y la implementaremos utilizando un clúster de host/host separado. cola de bloqueo, evolucionó a una "cola de mensajes"

La biblioteca estándar de Java ya proporciona una implementación lista para usar de colas de bloqueo

 Array Esta versión es más rápida, pero solo si conoce el número máximo de elementos.

Si no sabe cuántos elementos hay, es más apropiado usar Linked (para Array, la expansión frecuente es una gran sobrecarga)

Para BlockingQueue, la oferta y la encuesta no tienen funciones de bloqueo, mientras que las de poner y quitar sí las tienen.

Escriba un modelo productor-consumidor simple basado en la cola de bloqueo:

Un hilo produce y un hilo consume.

//生产者消费者模型

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

public class Demo19 {
    public static void main(String[] args) {
        //搞一个阻塞队列,作为交易场所
        BlockingQueue<Integer> queue = new LinkedBlockingDeque<>();

        //负责生产元素
        Thread t1 = new Thread(() ->{
            int count = 0;
            while(true){
                try {
                    queue.put(count);
                    System.out.println("生产元素: " + count);
                    count++;

                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        //负责消费元素
        Thread t2 = new Thread(() ->{
            while(true){
                try {
                    Integer n = queue.take();
                    System.out.println("消费元素: " + n);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t2.start();
    }
}

Implemente una cola de bloqueo usted mismo:

Aquí, la cola de bloqueo se implementa en base a matrices y colas circulares.

Conectamos las matrices de extremo a extremo para formar un anillo: cuando la cabeza y la cola se superponen, ¿están vacías o llenas?

(1) Se desperdicia una rejilla, cuando la cola alcanza la posición anterior de la cabeza, se considera llena.

(2) Cree una variable separada y use una variable separada para representar el número actual de elementos.

Ahora, hemos implementado una cola simple:

class MyBlockingQueue{
    //使用一个 String 类型的数组来保存元素,假设这里只存 String
    private String[] items = new String[1000];
    //指向队列的头部
    private int head = 0;
    //指向队列的尾部的下一个元素,总的来说,队列中的有效元素范围:[head,tail)
    //当 head 和 tail 相等(重合),相当于空的队列
    private int tail = 0;
    //使用 size 来表示元素个数
    private int size = 0;

    //入队列
    public void put(String elem){
        if (size >= items.length){
            //队列满了
            return;
        }
        items[tail] = elem;
        tail++;
        if (tail >= items.length){
            tail = 0;
        }
        size++;
    }
    //出队列
    public String take(){
        if (size == 0){
            //队列为空,暂时不能出队列
            return null;
        }
        String elem = items[head];
        head++;
        if (head >= items.length){
            head = 0;
        }
        size--;
        return elem;
    }
}

A continuación, transformamos la cola ordinaria anterior en una cola de bloqueo.

(1) Seguridad del hilo

Primero, coloque y retire el bloqueo para garantizar la seguridad del subproceso cuando lo llamen varios subprocesos.

Entonces agregamos directamente sincronizado para poner y tomar. 

Además del bloqueo, también se deben considerar los problemas de visibilidad de la memoria.

En este momento, agregamos volátiles a la cabeza, la cola y el tamaño.

(2) Implementar bloqueo

(1) Cuando la cola está llena, otra colocación provocará el bloqueo.

(2) Cuando la cola está vacía, volver a tomarla provocará el bloqueo.

Las dos esperas aquí no aparecerán al mismo tiempo, espere aquí o espere al otro lado. 

En el código anterior, si se cumple la condición, se realiza la espera. Cuando se despierta la espera, ¿se romperá la condición?

Por ejemplo, actualmente mi operación de venta está bloqueada porque la cola está llena. Después de un tiempo, se activa la espera. Cuando se despierte, ¿la cola estará llena en este momento? ¿Es posible que la cola siga llena?

En caso de reactivación, la cola aún está llena, lo que significa que el código posterior continúa ejecutándose y los elementos almacenados anteriormente pueden sobrescribirse.

En el código actual, si se despierta mediante una interrupción, se producirá una excepción directamente en este momento y el método finalizará y no continuará ejecutándose, lo que no causará el problema de sobrescribir los elementos existentes que acabamos de mencionar.

Pero si se escribe de acuerdo con el método try-catch, una vez que se activa la interrupción, el código bajará e ingresará al catch. Después de ejecutar el catch, el código no finalizará, sino que continuará ejecutándose, lo que activará la lógica del "elemento de anulación"

El análisis anterior encontró que este código fue escrito correctamente por suerte. Si se cambia ligeramente, ocurrirán problemas. Al escribirlo así, es difícil notar este detalle. ¿Hay alguna manera de hacer que este código sea infalible, incluso Si está escrito en forma try-catch, ¿está todo bien?

Solo deja esperar, despierta y luego juzga la condición nuevamente.

Si la condición sigue siendo que la cola está llena, continúe esperando. Si la condición es que la cola no está llena, puede continuar con la ejecución.

El propósito de while aquí no es hacer un bucle, sino utilizar un bucle para implementar inteligentemente la espera y confirmar las condiciones nuevamente después de despertarse. 

Por lo tanto, cuando se utiliza wait, se recomienda utilizar while para la determinación condicional.

El código final es el siguiente: 

class MyBlockingQueue{
    //使用一个 String 类型的数组来保存元素,假设这里只存 String
    private String[] items = new String[1000];
    //指向队列的头部
    volatile private int head = 0;
    //指向队列的尾部的下一个元素,总的来说,队列中的有效元素范围:[head,tail)
    //当 head 和 tail 相等(重合),相当于空的队列
    volatile private int tail = 0;
    //使用 size 来表示元素个数
    volatile private int size = 0;

    //入队列
    public synchronized void put(String elem) throws InterruptedException {
        while (size >= items.length){
            //队列满了,
            this.wait();
        }
        items[tail] = elem;
        tail++;
        if (tail >= items.length){
            tail = 0;
        }
        size++;
        //用来唤醒队列为 空 的阻塞情况
        this.notify();
    }
    //出队列
    public synchronized String take() throws InterruptedException {
        while (size == 0){
            //队列为空,暂时不能出队列
            this.wait();
        }
        String elem = items[head];
        head++;
        if (head >= items.length){
            head = 0;
        }
        size--;
        //使用这个 notify 来唤醒队列满的阻塞情况
        this.notify();
        return elem;
    }
}

3. Temporizador

Los temporizadores también son componentes comunes en el desarrollo diario, similar a un despertador.

TimerTask es similar al Runnable que aprendimos antes: registrará una hora: cuando se ejecutará la tarea actual.

La tarea registrada en el temporizador no se ejecuta en el hilo que llama al programa, sino que la ejecuta el hilo dentro del temporizador.

import java.util.Timer;
import java.util.TimerTask;

public class demo21 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        //给 timer 中注册的这个任务,不是在调用 schedule 的线程中执行的,而是通过 Timer 内部的线程来负责执行的
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时任务执行");
            }
        },3000);
        System.out.println("程序开始运行");
    }
}

Encontraremos que después de ejecutar los resultados, el código no finaliza el proceso después de imprimir:

 Esto es porque:

El temporizador tiene su propio hilo internamente. Para garantizar que las tareas recién programadas se puedan procesar en cualquier momento, este hilo continuará ejecutándose y este hilo también es un hilo en primer plano. 

A continuación, intente implementar un temporizador usted mismo:

¡Un cronómetro puede tener muchas tareas!

Primero, debe poder describir una tarea y luego utilizar una estructura de datos para organizar múltiples tareas.

(1) Cree una clase como TimerTask para representar una tarea. Esta tarea debe incluir dos aspectos: el contenido de la tarea y el tiempo de ejecución real de la tarea.

El tiempo de ejecución real de la tarea se puede representar mediante una marca de tiempo. Al programar, primero obtenga la hora actual del sistema y luego, en función de esto, agregue el intervalo de tiempo de retraso para obtener el tiempo real para ejecutar la tarea.

(2) Utilice una determinada estructura de datos para organizar múltiples TimerTasks

Si usa Lista (matriz, lista vinculada) para organizar Timertask, si hay demasiadas tareas, ¿cómo determinar qué tarea se puede ejecutar y cuándo?

De esta manera, debe crear un hilo para recorrer continuamente la Lista anterior para ver si cada elemento aquí ha alcanzado el tiempo. Cuando se acabe el tiempo, se ejecutará. Si no se acaba el tiempo, se omitirá.

Pero esta idea no es científica. Si el momento para realizar estas tareas aún es demasiado pronto, el hilo de escaneo aquí deberá escanear repetidamente antes de que llegue el momento. Este proceso es muy ineficiente.

(a) No es necesario escanear todas las tareas, solo concéntrese en las tareas más tempranas.

Si el tiempo de la tarea más temprano aún no ha llegado, los tiempos de otras tareas no llegarán aún más.

Se mejoró atravesar todas las tareas para centrarse en una sola.

Entonces, ¿cómo saber qué tarea es la máxima prioridad?

Lo más apropiado es utilizar una cola prioritaria para organizar todas las tareas, el primer elemento del equipo es la tarea con menor tiempo.

(b) No es necesario realizar el escaneo repetidamente para esta tarea.

En cambio, después de obtener la hora del primer elemento de la cola, se hace una diferencia con la hora actual del sistema y se determina el tiempo de suspensión/espera en función de esta diferencia. Antes de alcanzar esta hora, no se realizarán escaneos repetidos. lo que reduce en gran medida el número de escaneos

"Mejorar la eficiencia" aquí no significa acortar el tiempo de ejecución, sino reducir la utilización de recursos y evitar el desperdicio innecesario de CPU.

(c) Crear un hilo de escaneo que sea responsable de monitorear si la tarea del elemento principal del equipo ha expirado.

El proceso central de este código es el siguiente:

import java.util.PriorityQueue;

//创建一个类,用来描述定时器中的一个任务
class MyTimerTask{
    //任务啥时候执行,毫秒级的时间戳
    private long time;
    //任务具体是啥
    private Runnable runnable;

    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public MyTimerTask(Runnable ruunnabl, long delay){
        //delay 是一个相对的时间差
        //构造 time 要根据当前系统时间和 deley 进行构造
        time = System.currentTimeMillis() + delay;
        this.runnable = ruunnabl;
    }
}

//定时器类的本体
class MyTimer{
    //使用优先级队列,来保存上述的 N 个任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    //定时器的核心方法,就是把要执行的任务给添加到队列中
    public void schedule(Runnable runnable,long delay){
        MyTimerTask task = new MyTimerTask(runnable,delay);
        queue.offer(task);
    }

    //MyTimer 中,还需要构造一个 “扫描线程” ,一方面去负责监控队首元素是否到点了们是否应该执行;一方面当任务到店之后
    // 就要调用这里的 Runnable 的 Run 方法来完成任务
    public MyTimer(){
        //扫描线程
        Thread t = new Thread(() ->{
            while(true){
                if (queue.isEmpty()){
                    //注意,当前队列为空,此时就不应该去取这里的元素
                    continue;
                }
                MyTimerTask task = queue.peek();
                long curTime = System.currentTimeMillis();
                if (curTime >= task.getTime()){
                    //需要执行任务
                    queue.poll();
                    task.getRunnable().run();
                }else {
                    //让当前扫描线程休眠一下,就可以按照时间差进行休眠
                    try {
                        Thread.sleep(task.getTime() - curTime);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t.start();
    }
}

En el código anterior, se ha escrito la lógica central del temporizador, pero todavía hay varios problemas clave en este código:

1. No es seguro para subprocesos

 Esta clase de colección no es segura para subprocesos y se utilizará tanto en el subproceso principal como en el subproceso de escaneo.

Bloquear operaciones en la cola

2. En el hilo de escaneo, ¿es apropiado usar dormir directamente para dormir?

inadecuado

(a) Después de que el modo de suspensión entre en bloqueo, el bloqueo no se liberará, lo que afectará a otros subprocesos y al programa ejecutado aquí.

(b) Durante el sueño, es inconveniente interrumpir temprano (aunque puede usar la interrupción para interrumpir, pero interrumpir significa que el hilo debe terminar)

Supongamos que la tarea principal actual se ejecuta a las 14:00 y la hora actual es las 13:00 (dormir una hora). Si agregamos una nueva tarea a esta hora, la nueva tarea se ejecutará a las 13:30. esta vez, la nueva tarea se ejecutará a las 13:30 y se convertirá en la primera tarea en ejecutarse.

Esperamos que cada vez que llegue una nueva tarea, podamos despertar el estado inactivo anterior y hacer un nuevo juicio basado en el último estado de la tarea.

En este momento, el hilo de escaneo puede bloquearse y esperar 30 minutos.

Por el contrario, esperar es más apropiado : esperar también puede especificar un tiempo de espera y esperar también puede activarse con anticipación.

3. Si escribe cualquier clase, ¿se pueden colocar sus objetos en la cola de prioridad?

Requiere que los elementos colocados en la cola de prioridad se hagan "comparables"

Definir reglas de comparación entre tareas a través de Comparable o Comparator

Aquí, aquí mismo en MyTimerTask, se implementa Comparable

El código final es el siguiente:

import java.util.PriorityQueue;

//创建一个类,用来描述定时器中的一个任务
class MyTimerTask implements Comparable<MyTimerTask>{
    //任务啥时候执行,毫秒级的时间戳
    private long time;

    @Override
    public int compareTo(MyTimerTask o) {
        //把时间小的,优先级高,最终时间最小的元素,就会放到队首
        return (int) (this.time - o.time);
    }

    //任务具体是啥
    private Runnable runnable;

    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public MyTimerTask(Runnable ruunnabl, long delay){
        //delay 是一个相对的时间差
        //构造 time 要根据当前系统时间和 deley 进行构造
        time = System.currentTimeMillis() + delay;
        this.runnable = ruunnabl;
    }
}

//定时器类的本体
class MyTimer{
    //用来加锁的对象
    private Object locker = new Object();

    //使用优先级队列,来保存上述的 N 个任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    //定时器的核心方法,就是把要执行的任务给添加到队列中
    public void schedule(Runnable runnable,long delay){
        synchronized (locker)
        {
            MyTimerTask task = new MyTimerTask(runnable,delay);
            queue.offer(task);
            //每次来新的任务,都唤醒一下之前的扫描线程,好让扫描线程根据最新的额任务情况重新规划等待时间
            locker.notify();
        }
    }

    //MyTimer 中,还需要构造一个 “扫描线程” ,一方面去负责监控队首元素是否到点了们是否应该执行;一方面当任务到店之后
    // 就要调用这里的 Runnable 的 Run 方法来完成任务
    public MyTimer(){
        //扫描线程
        Thread t = new Thread(() ->{
            while(true){
                synchronized (locker){
                    while (queue.isEmpty()){
                        //注意,当前队列为空,此时就不应该去取这里的元素
                        //此处使用 wait 等待更合适,如果使用 continue ,就会使这个线程的 while 循环运行的飞快
                        //也会陷入一个高频占用 cpu 的状态(忙等)
                        try {
                            locker.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    MyTimerTask task = queue.peek();
                    long curTime = System.currentTimeMillis();
                    if (curTime >= task.getTime()){
                        //需要执行任务
                        queue.poll();
                        task.getRunnable().run();
                    }else {
                        try {
                            //让当前扫描线程休眠一下,就可以按照时间差进行休眠
                            locker.wait(task.getTime() - curTime);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        });
        t.start();
    }
}

4. Grupo de subprocesos

Pool es un método muy importante

Si necesitamos crear y destruir subprocesos con frecuencia, no se puede ignorar el costo de crear y destruir subprocesos en este momento.

Por lo tanto, puede usar el grupo de subprocesos para crear una ola de subprocesos con anticipación. Si necesita usar el subproceso más adelante, simplemente tome uno del grupo. Cuando el subproceso ya no se use, vuelva a colocarlo en el grupo.

Originalmente, era necesario crear/destruir subprocesos, pero ahora los subprocesos ya preparados se obtienen del grupo y se devuelven al grupo.

¿Por qué recuperar algo del grupo es más rápido y eficiente que crear subprocesos desde el sistema?

Si crea un hilo desde el sistema, debe llamar a la API del sistema y luego el kernel del sistema operativo completa el proceso de creación del hilo.

El kernel proporciona servicios a todos los procesos , por lo que esto es incontrolable. 

Si el subproceso se obtiene del grupo de subprocesos, las operaciones anteriores en el kernel se han realizado de antemano. El proceso actual de obtención del subproceso se completa mediante código de usuario puro (modo de usuario puro) y controlable .

La biblioteca estándar de Java también proporciona un grupo de subprocesos listo para usar.

 Patrón de fábrica: producir objetos.

Generalmente, los objetos se crean mediante new y mediante el método constructor.

Sin embargo, el método constructor tiene un defecto importante: el nombre del método constructor está fijado al nombre de la clase.

Algunas clases requieren múltiples métodos de construcción diferentes, pero el nombre del método de construcción es fijo y solo se puede implementar mediante la sobrecarga del método (la cantidad y el tipo de parámetros deben ser diferentes)

Por ejemplo: ahora queremos construir de dos maneras, una es construir según las coordenadas del producto cartesiano y la otra es construir según las coordenadas polares. El número y tipo de parámetros en estos dos métodos de construcción son los mismos, y no puede constituir una sobrecarga. , se informará un error durante la compilación

En este punto, puede utilizar el modo de fábrica para resolver los problemas anteriores.

Utilice el patrón de fábrica para resolver los problemas anteriores: en lugar de utilizar constructores, utilice métodos ordinarios para construir objetos. El nombre de dicho método puede ser arbitrario. Dentro del método ordinario, cree un nuevo objeto.

Dado que el propósito de los métodos ordinarios es crear objetos, dichos métodos son generalmente estáticos.

Una vez que el objeto del grupo de subprocesos esté listo, puede agregar tareas al grupo de subprocesos utilizando el método de envío.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//线程池
public class Demo23 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(4);
        for (int i = 0;i < 100;i++){
            service.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }
}

Además de los grupos de subprocesos anteriores, la biblioteca estándar también proporciona un grupo de subprocesos con una interfaz más rica: ThreadPoolExecutor

Los grupos de subprocesos anteriores se han encapsulado para facilitar su uso. ThreadPoolExecutor tiene muchas opciones para que las ajustemos, lo que puede satisfacer mejor nuestras necesidades.

Hablemos de los parámetros y el significado del método de construcción del grupo de subprocesos en la biblioteca estándar de Java.

Si se compara el grupo de subprocesos con una empresa, la cantidad de subprocesos centrales es la cantidad de empleados formales.

El número máximo de hilos es el número de empleados regulares + pasantes

Cuando el negocio de la empresa no está ocupado, no necesita pasantes, cuando el negocio de la empresa está ocupado, puede encontrar algunos pasantes para compartir las tareas.

Esto se puede hacer, no sólo para garantizar un procesamiento eficiente de las tareas cuando está ocupado, sino también para garantizar que los recursos no se desperdicien cuando están inactivos.


Los subprocesos internos pueden destruirse si su inactividad excede un cierto umbral.


Hay muchas tareas dentro del grupo de subprocesos y estas tareas se pueden gestionar mediante colas de bloqueo. 

El grupo de subprocesos puede tener una cola de bloqueo incorporada y usted puede especificar una manualmente


Patrón de fábrica, crea hilos a través de esta clase de fábrica 


Este es el enfoque de la inspección del grupo de subprocesos: método de rechazo / estrategia de rechazo

El grupo de subprocesos tiene una cola de bloqueo. Cuando la cola de bloqueo está llena, se continúan agregando tareas. ¿Cómo lidiar con esto?


Primera estrategia de rechazo:

Lanza una excepción directamente y el grupo de subprocesos dejará de funcionar. 


Segunda estrategia de rechazo:

Quien sea el hilo que agregue esta nueva tarea ejecutará esta tarea


Tercera estrategia de rechazo:

Descartar la tarea más antigua y ejecutar la nueva tarea


La cuarta estrategia de rechazo:

Descarta directamente la nueva tarea y continúa con la tarea anterior.

Hay dos grupos de grupos de subprocesos mencionados anteriormente: un grupo de grupos de subprocesos son Ejecutores encapsulados y un grupo de grupos de subprocesos son ThreadPoolExecutors nativos. Puede utilizar cualquiera de ellos, principalmente según las necesidades reales.

A continuación, intentamos simular e implementar un grupo de subprocesos nosotros mismos:


import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

class MyThreadPool{
    private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();

    //通过这个方法,来把任务添加到线程池中
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    // n 表示线程池里面有几个线程
    //创建了一个有固定数量的线程池
    public MyThreadPool(int n){
        for(int i = 0;i < n;i++){
            Thread t = new Thread(() ->{
                while(true){
                    //循环的取出任务,并执行
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
        }
    }
}

//线程池
public class Demo24 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(4);
        for(int i = 0;i < 1000;i++){
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    //要执行的工作
                    System.out.println(Thread.currentThread().getName() + " hello");
                }
            });
        }
    }
}

Dado que la programación en este momento es aleatoria, cuando se ejecutan las tareas actualmente insertadas en este grupo de subprocesos, la carga de trabajo de los N subprocesos puede no ser completamente igual, pero en un sentido estadístico, las cargas de tareas son iguales.

Además, hay otra pregunta importante:

Al crear un grupo de subprocesos, ¿de dónde proviene la cantidad de subprocesos?

En diferentes proyectos, el trabajo realizado por los hilos es diferente.

El trabajo de algunos subprocesos es "intensivo en CPU" y el trabajo de subprocesos son todos cálculos.

La mayor parte del trabajo debe realizarse en la CPU. La CPU debe organizar los núcleos para completar el trabajo antes de que pueda avanzar. Si la CPU tiene N núcleos y el número de subprocesos también es N, la situación ideal es tener uno en cada núcleo.

Si hay muchos subprocesos, los subprocesos están esperando en la cola y no habrá nuevos avances.

El trabajo de algunos subprocesos es "intensivo en IO", incluida la lectura y escritura nocturna, la espera de la entrada del usuario y la comunicación de red.

Implica mucho tiempo de espera. Durante el proceso de espera, la CPU no se utiliza, incluso si hay más subprocesos, no supondrá demasiada carga para la CPU.

En el desarrollo real, parte del trabajo de un subproceso a menudo requiere un uso intensivo de la CPU y parte de su trabajo requiere un uso intensivo de IO.

En este momento, no se sabe cuántos subprocesos se están ejecutando en la CPU y cuántos están esperando IO.

Un mejor enfoque aquí es encontrar la cantidad adecuada de subprocesos mediante experimentos, probar diferentes números de subprocesos mediante pruebas de rendimiento y, durante el proceso de prueba, encontrar un valor que esté más equilibrado entre el rendimiento y la sobrecarga de recursos del sistema.


6. Estrategias de bloqueo comunes

La estrategia de bloqueo que se explica a continuación no se limita a Java. Cualquier tema relacionado con " bloqueo " puede incluir el siguiente contenido .
Estas características son principalmente de referencia para los implementadores de bloqueos .
La estrategia de bloqueo no se refiere a un bloqueo específico, es un concepto abstracto que describe las características de un bloqueo.

1. Bloqueo optimista versus bloqueo pesimista

Bloqueo optimista: prediga que es poco probable que se produzcan conflictos de bloqueo en este escenario

Bloqueo pesimista: al predecir este escenario, es muy fácil tener un conflicto de bloqueo

Conflicto de bloqueo: dos subprocesos intentan adquirir un bloqueo, un subproceso puede adquirirlo con éxito y el otro subproceso se bloquea y espera.

Si la probabilidad de conflicto de bloqueo es alta o baja tendrá un cierto impacto en el trabajo posterior.


2. Candado pesado versus candado liviano

Bloqueo pesado: el costo del bloqueo es relativamente alto (lleva más tiempo y menos recursos del sistema) 

Bloqueo liviano: el costo del bloqueo es relativamente pequeño (lleva menos tiempo y consume menos recursos del sistema)

Es probable que un candado pesimista sea un candado pesado (no absoluto)

Una cerradura optimista, probablemente también una cerradura ligera (no absoluta)

El pesimismo y el optimismo se basan en la predicción de la probabilidad de conflictos de bloqueo antes del bloqueo, lo que determina la cantidad de trabajo; el peso ligero se basa en considerar la sobrecarga real del bloqueo después del bloqueo.

Oficialmente, debido a que estos conceptos se superponen, para un candado específico, se le puede llamar candado optimista o candado liviano.


3. Bloqueo de giro VS suspender bloqueo de espera

Bloqueo giratorio: es una implementación típica de bloqueo ligero.

A menudo significa que en el modo de usuario, mediante la autoselección (bucle while), se logra un efecto similar a un bloqueo.

Consumirá una cierta cantidad de recursos de la CPU, pero puede bloquearse lo más rápido posible. 

Bloqueo de espera suspendido: es una implementación típica de bloqueo pesado

A través del estado del kernel y el mecanismo de bloqueo proporcionado por el sistema, cuando ocurre un conflicto de bloqueo, implicará la programación de subprocesos del kernel, lo que provocará que el subproceso en conflicto se cuelgue (bloquee y espere).


4. Bloqueo de lectura-escritura VS bloqueo mutex

Bloqueo de lectura y escritura: separa el bloqueo de operaciones de lectura y el bloqueo de operaciones de escritura

Varios subprocesos que leen la misma variable al mismo tiempo no implican problemas de seguridad de subprocesos.

Si hay dos subprocesos, un subproceso lee y bloquea , y el otro subproceso también lee y bloquea , no habrá competencia de bloqueo.

Si hay dos subprocesos, un subproceso está bloqueado contra escritura y el otro subproceso también está bloqueado contra escritura , se producirá competencia de bloqueo.

Si hay dos subprocesos, uno está bloqueado para escribir y el otro también está bloqueado para lectura , se producirá competencia de bloqueo.

En el desarrollo real, la frecuencia de las operaciones de lectura suele ser mucho mayor que la de las operaciones de escritura. 

La biblioteca estándar de Java también proporciona bloqueos de lectura y escritura listos para usar.


5. Bloqueo justo VS bloqueo injusto

Como se define aquí, un candado justo es un candado por orden de llegada.

Los bloqueos injustos parecen tener la misma probabilidad, pero en realidad son injustos (el tiempo de bloqueo de cada hilo es diferente)

El bloqueo propio del sistema operativo (pthread_mutex) es un bloqueo injusto.

Para implementar bloqueos justos, se necesitan algunas estructuras de datos adicionales para admitirlo (por ejemplo, debe haber una manera de registrar el tiempo de espera de bloqueo de cada hilo).


6. Bloqueo reentrante VS bloqueo no reentrante

Si un hilo bloquea un candado dos veces seguidas, se producirá un punto muerto, que es un candado no reentrante.

Si el código se escribe así, ¿habrá algún problema?

1. Llame al método y primero bloquee esto. Suponga que el bloqueo se realizó correctamente en este momento.

2. A continuación, ejecute hacia abajo para sincronizar en el bloque de código. En este momento, esto todavía está bloqueado.

En este momento, se producirá competencia de bloqueo porque este objeto ya está en un estado bloqueado.

En este momento, el hilo se bloqueará hasta que se libere el bloqueo antes de que tenga la oportunidad de obtenerlo.

En este código, el bloqueo solo se puede liberar después de ejecutar el método de aumento. Sin embargo, el bloqueo debe adquirirse con éxito la segunda vez antes de que el método pueda continuar ejecutándose.

Si desea que el código continúe ejecutándose hacia abajo, debe obtener el segundo bloqueo, es decir, liberar el primer bloqueo. Si desea liberar el primer bloqueo, debe asegurarse de que el código continúe ejecutándose.

En este momento, debido a que el bloqueo no se puede liberar, el código está atascado aquí y el hilo está congelado (la primera manifestación de un punto muerto)

Si es un bloqueo no reentrante, el bloqueo no guardará qué hilo lo bloqueó. Siempre que reciba una solicitud de "bloqueo", rechazará el bloqueo actual, independientemente de qué hilo sea el hilo actual. , se producirá un punto muerto ocurrir

Un bloqueo reentrante guardará qué hilo agregó el bloqueo. Después de recibir una solicitud de bloqueo posterior, primero comparará para ver si el hilo bloqueado es el hilo que actualmente contiene el bloqueo. En este momento, puede decidir con flexibilidad.

sincronizado en sí es un bloqueo reentrante, por lo que este código no se bloqueará.

Los bloqueos reentrantes permiten que el bloqueo registre qué hilo contiene actualmente el bloqueo.

Si el bloqueo es de N niveles, cuando encuentra }, ¿cómo sabe la JVM si el } actual es el más externo?

Simplemente deje que el candado contenga un contador, deje que el objeto de bloqueo no solo registre qué subproceso retiene el candado, sino que también registre cuántas veces el subproceso actual ha agregado el candado a través de una variable entera.

Cada vez que se encuentra una operación de bloqueo, el contador es +1, y cada vez que se encuentra una operación de desbloqueo, el contador es -1.

Cuando el contador se reduce a 0, la operación de apertura de la cerradura se realiza realmente y no se libera en otros momentos.

A este conteo lo llamamos "conteo de referencia".


7. Punto muerto

1. Un hilo, un candado, pero es un candado no repetible. Si el hilo bloquea el candado dos veces seguidas, se producirá un punto muerto.

2. Dos subprocesos, dos candados: los dos subprocesos primero adquieren un candado respectivamente y luego intentan adquirir el candado de la otra parte al mismo tiempo.

3. N hilos, N cerraduras

Siente el punto muerto a través del código;

public class Demo25 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() ->{
            synchronized (locker1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker2){
                    System.out.println("t1 两把锁加锁成功");
                }
            }
        });
        
        Thread t2 = new Thread(() ->{
            synchronized (locker2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker1){
                    System.out.println("t2 两把锁加锁成功");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

Este código es un segundo tipo típico de interbloqueo. Después de ejecutarlo, encontraremos que ninguno de los subprocesos puede imprimir el resultado con éxito.

 Podemos abrir jconsole para observar más a fondo el hilo.

Descubriremos que ambos hilos están atascados en otros lugares de bloqueo.

Si se trata de un programa de servidor y se produce un punto muerto, el hilo bloqueado se congelará y no podrá continuar funcionando, lo que tendrá un impacto grave en el programa.

3. N hilos, N cerraduras

Problema de los filósofos gastronómicos:

Supongamos que hay cinco filósofos y 5 palillos sobre la mesa. 

Todo filósofo tiene que hacer principalmente dos cosas:

1. Piensa en la vida y deja los palillos.

2. Al comer fideos, puede levantar los palillos con la mano izquierda y derecha y recoger los fideos para comer.

 Otros ajustes:

1. No se sabe con certeza cuándo cada filósofo pensará en la vida y cuándo comerá fideos.

2. Todo filósofo, una vez que quiera comer fideos, será muy terco para completar la operación de comer fideos. Si otros usan sus palillos, será bloqueado y esperará, y durante el proceso de espera, no los dejará. sus manos sosteniendo palillos

Según la configuración del modelo anterior, todos estos filósofos pueden funcionar muy bien.

Sin embargo, si ocurre una situación extrema, se producirá un punto muerto.

Supongamos que en el mismo momento, cinco filósofos quieren comer fideos y, al mismo tiempo, extienden la mano izquierda, recogen los palillos de la izquierda y luego intentan recoger los palillos de la derecha.

5 filósofos son 5 hilos, 5 palillos son 5 candados

El modelo filósofo describe vívidamente el tercer tipo de problema de punto muerto.

Entonces, ¿hay alguna manera de evitar el estancamiento?

Primero, aclaremos las razones del punto muerto y las condiciones necesarias para el punto muerto.

Cuatro condiciones necesarias: (una es indispensable)

Siempre que se pueda violar cualquiera de las condiciones, se puede evitar el punto muerto.

1. Se utiliza exclusión mutua: después de que un subproceso adquiere un bloqueo, otros subprocesos no pueden adquirirlo.

Las cerraduras realmente utilizadas son generalmente mutuamente excluyentes (las características básicas de las cerraduras)

2. No se puede anular. El bloqueo sólo puede ser liberado activamente por el titular y no puede ser arrebatado por otros hilos.

También es la característica básica de las cerraduras.

3. Solicitar y mantener. Un hilo intenta adquirir múltiples bloqueos. Durante el proceso de intentar adquirir el segundo bloqueo, mantendrá el estado de adquisición del primer bloqueo.

Depende de la estructura del código (lo más probable es que afecte los requisitos)

4. Esperando en un bucle, t1 intenta obtener el casillero2, necesita que t2 termine de ejecutarse, libera el casillero2, t2 intenta obtener el casillero1, necesita que t1 termine de ejecutarse, libera el casillero1

Depende de la estructura del código (puntos clave para resolver el problema del punto muerto)

¿Cómo resolver específicamente el problema del punto muerto? Existen muchos métodos prácticos (Algoritmo bancario)

El algoritmo bancario puede resolver el problema del punto muerto, pero no es muy práctico.

Aquí presentamos una forma más simple y efectiva de resolver el problema del punto muerto.

Numere las cerraduras y especifique el orden en que deben cerrarse.

Por ejemplo: se estipula que si cada subproceso desea adquirir varios candados, primero debe adquirir el candado con el número menor y luego adquirir el candado con el número mayor.

Siempre que el orden de bloqueo de todos los subprocesos siga estrictamente el orden anterior, no habrá espera para el bloqueo.


8. Principio sincronizado

1. Funciones básicas 

¿Qué estrategias específicas utiliza sincronizado?

1. Sincronizado es tanto un bloqueo pesimista como un bloqueo optimista.

2. Sincronizado Incluso las cerraduras pesadas son cerraduras livianas

3. La parte de bloqueo sincronizada de peso pesado se implementa en función del bloqueo mutex del sistema, y ​​la parte de bloqueo liviana se implementa en función del bloqueo giratorio.

4. Sincronizado es un bloqueo injusto (no sigue el principio de primero en llegar, primero en ser atendido. Una vez que se libera el bloqueo, qué hilo obtiene el bloqueo depende de su capacidad)

5. Sincronizado es un bloqueo reentrante (registrará internamente qué subproceso obtuvo el bloqueo y registrará el recuento de referencias)

6. Sincronizado no es un bloqueo de lectura y escritura


2. Proceso de bloqueo 

La estrategia de implementación interna (principio interno) de sincronizado.

Después de escribir una sincronización en el código, aquí pueden ocurrir una serie de "procesos adaptativos" y actualizaciones de bloqueo (expansión de bloqueo).

Sin bloqueo->bloqueo sesgado->bloqueo ligero->bloqueo pesado

El bloqueo sesgado no es un bloqueo real, sino solo una "marca". Si otros subprocesos compiten por el bloqueo, el bloqueo estará realmente bloqueado. Si no hay otros subprocesos compitiendo, el bloqueo no estará realmente bloqueado de principio a fin. bloqueado

El candado en sí tiene un costo determinado. Si no puedes agregarlo, no lo agregues. Alguien tendrá que competir para agregar el candado.

Cerraduras livianas, sincronizadas implementan cerraduras livianas mediante cerraduras giratorias

Si el bloqueo está ocupado aquí, otro hilo consultará repetidamente si el estado del bloqueo actual se ha liberado de acuerdo con el método de giro.

Sin embargo, si más y más subprocesos compiten por este bloqueo en el futuro (los conflictos de bloqueo se vuelven más intensos), sincronizado se actualizará de un bloqueo liviano a un bloqueo pesado.

Eliminación de bloqueo, el compilador determinará de manera inteligente si el código actual debe bloquearse. Si escribe el bloqueo primero, pero en realidad no es necesario bloquearlo, la operación de bloqueo se eliminará automáticamente.

El compilador realiza una optimización para garantizar que la lógica después de la optimización sea coherente con la lógica anterior.

Esto hará que algunas optimizaciones se vuelvan conservadoras.

Los programadores no podemos confiar únicamente en la optimización para mejorar la eficiencia del código, nosotros mismos también debemos desempeñar algún papel. Determinar cuándo bloquear también es una tarea muy importante.

rugosidad de la cerradura

 "Granularidad de bloqueo": si la operación de bloqueo contiene más código del que realmente se ejecuta, se considera que la granularidad de bloqueo es mayor.

A veces, esperamos que la granularidad del bloqueo sea menor y el grado de concurrencia sea mayor.

A veces, es mejor tener una granularidad de bloqueo mayor, porque el bloqueo y el desbloqueo también tienen una sobrecarga.


 9. CAS

1. Concepto 

CAS: nombre completo Comparar e intercambiar , significado literal : " Comparar e intercambiar " ,
Capaz de comparar e intercambiar el valor en un determinado registro con el valor en la memoria para ver si son iguales , si son iguales, intercambiar el valor en otro registro con la memoria.
boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
}

 Este es un fragmento de pseudocódigo.

Basado en CAS, se puede derivar un conjunto de programación "sin bloqueos"

El alcance de uso de CAS tiene ciertas limitaciones.


2. Aplicación de CAS

(1) Implementar la clase atómica

Por ejemplo, varios subprocesos realizan ++ en una variable de recuento

En la biblioteca estándar de Java, se ha proporcionado un conjunto de clases atómicas.

Dentro de la clase atómica, no se realizan operaciones de bloqueo, solo se completa el incremento automático seguro para subprocesos a través de CAS.

import java.util.concurrent.atomic.AtomicInteger;

//使用原子类
public class Demo26 {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() ->{
           for(int i = 0;i < 50000;i++){
               //相当于 count++
               count.getAndIncrement();
               //相当于 ++count
               //count.incrementAndGet();
               //count--
               //count.getAndDecrement();
               //--count
               //count.decrementAndGet();
           }
        });

        Thread t2 = new Thread(() ->{
           for (int i = 0;i < 50000;i++){
               count.getAndIncrement();
           }
        });

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

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

        System.out.println(count.get());
    }
}

La clase atómica anterior se implementa en base a CAS.

Implementación de pseudocódigo:

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

Cuando dos subprocesos se ejecutan ++ al mismo tiempo, sin ninguna restricción, significa que los dos ++ son seriales y se pueden calcular correctamente. A veces, las dos operaciones ++ se intercalan, pueden surgir problemas en este momento.

El bloqueo garantiza la seguridad de la memoria del subproceso y, mediante el bloqueo, se evita por la fuerza el entrelazado. 

La clase atómica / CAS garantiza la seguridad de los subprocesos: utilice CAS para identificar si actualmente hay "entrelazado". Si no hay entrelazado, modifíquelo directamente en este momento, lo cual es seguro; si ocurre entrelazado, vuelva a leer el último valor en la memoria. y repítelo de nuevo. Intenta modificar.

CAS es una instrucción y la instrucción en sí no se puede dividir.

CAS en sí es una instrucción única, que en realidad incluye la operación de acceder a la memoria. Cuando varias CPU intentan acceder a la memoria, esencialmente habrá una secuencia.

¿Por qué estos dos CAS acceden a la memoria de forma secuencial?

Múltiples CPU que operan el mismo recurso también involucrarán competencia de bloqueo (bloqueos de nivel de instrucción), este bloqueo es mucho más liviano que los bloqueos de nivel de código sincronizados que generalmente llamamos


(2) Implementar bloqueo de giro

Implemente bloqueos más flexibles basados ​​en CAS y obtenga más control .
Pseudocódigo de bloqueo de giro:
public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}


 3. Cuestiones ABA del CAS

El punto clave de CAS es comparar el valor del registro 1 y la memoria, y determinar si el valor de la memoria ha cambiado en función de si son iguales.

Si el valor de la memoria cambia, otros subprocesos lo han modificado.

Si el valor de la memoria no ha cambiado y ningún otro hilo lo ha modificado, las modificaciones posteriores son seguras.

Pero aquí hay un problema: si el valor aquí no ha cambiado, ¿significa que ningún otro hilo lo ha modificado?

A - B - A Problema: otro hilo cambia la variable de A -> B, y luego de B -> A. En este momento, el hilo actual no puede distinguir si el valor nunca ha cambiado o si ha cambiado y luego ha cambiado. atrás.

En la mayoría de los casos, incluso si hay un problema de ABA, no tendrá ningún impacto, pero si se encuentra con algunos escenarios extremos, puede que no sea el caso.

Supongamos que ahora hay 100 yuanes en la cuenta y desea retirar 50 yuanes.

Supongamos que ocurre una situación extrema: cuando presiona el primer botón para retirar dinero, se atasca, por lo que lo presiona nuevamente (se generan dos solicitudes de retiro y el cajero automático usa dos subprocesos para procesar estas dos solicitudes)

Suponiendo que los retiros se realicen según el método CAS, cada hilo funciona de la siguiente manera:

1. Leer el saldo de la cuenta y ponerlo en la variable M.

2. Utilice CAS para determinar si el monto real actual sigue siendo M. Si es así, modifique el saldo real a M - 50. De lo contrario, abandone la operación actual (la operación falla)

Cuando solo dos subprocesos t1 y t2 realizan la operación de retiro de 50, debido a CAS, el dinero solo se deducirá con éxito una vez. 

Pero supongamos que otra persona transfiere 50 yuanes a la cuenta en este momento, de modo que después de que se completa la operación t2, el saldo vuelve a los 100 yuanes iniciales, lo que hace que t1 piense erróneamente que ningún otro hilo ha modificado el saldo, por lo que continúa Se realizó la operación de deducción, lo que resultó en deducciones repetidas, en este momento ocurrió el problema A-B-A.

Aunque la probabilidad de las operaciones anteriores es relativamente pequeña, aún es necesario considerarla.

Con respecto al problema ABA, la idea básica de CAS está bien, pero la razón principal es que la operación de modificación puede saltar repetidamente, lo que fácilmente invalida el juicio de CAS.

CAS determina que "los valores son los mismos", pero en realidad espera que "los valores no hayan cambiado"

Si se acuerda que el valor sólo puede cambiar en una dirección (por ejemplo, sólo puede aumentar en una dirección y no puede disminuir), el problema se resolverá fácilmente.

Pero parece que el saldo de la cuenta no sólo puede aumentar sino no disminuir.

Aunque el equilibrio no funciona, ¡el número de versión sí! ! !

En este momento, para medir si el saldo ha cambiado, no solo miramos el valor del saldo, sino también el número de versión.

Consiga un vecino de al lado para el saldo de la cuenta: número de versión (el valor aumenta, no disminuye)

Utilice CAS para determinar si los números de versión son los mismos. Si los números de versión son los mismos, los datos no deben haber sido modificados. Si los datos han sido modificados, el número de versión debe aumentarse.


10. Clases comunes de JUC (java.util.concurrent)

concurrente: concurrencia (multiproceso)

1. Interfaz colaborable

También es una forma de crear hilos.

Runnable puede representar una tarea (método de ejecución) y devuelve void

Callable también puede representar una tarea (método de llamada) y devolver un valor específico. El tipo se puede especificar mediante parámetros genéricos.

Si está realizando operaciones de subprocesos múltiples, si solo le importa el proceso de ejecución de subprocesos múltiples, use Runnable.

Al igual que los grupos de subprocesos y los temporizadores, solo están relacionados con procesos y utilizan Runnable

Si le preocupan los resultados del cálculo de subprocesos múltiples, es más apropiado utilizar Callable

Para calcular una fórmula mediante subprocesos múltiples, es más apropiado utilizar Callable

Ejemplo de código: cree un hilo para calcular 1 + 2 + 3 + ... + 1000, sin usar la versión invocable
static class Result {
    public int sum = 0;
    public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
    Result result = new Result();
    Thread t = new Thread() {
        @Override
        public void run() {
            int sum = 0;
            for (int i = 1; i <= 1000; i++) {
                sum += i;
           }
            synchronized (result.lock) {
                result.sum = sum;
                result.lock.notify();
           }
       }
   };
    t.start();
    synchronized (result.lock) {
        while (result.sum == 0) {
            result.lock.wait();
       }
        System.out.println(result.sum);
   }
}
Ejemplo de código: cree un hilo para calcular 1 + 2 + 3 + ... + 1000, usando la versión invocable
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo27 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1;i <= 1000;i++){
                    sum += i;
                }
                return sum;
            }
        };

        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        
        Integer result = futureTask.get();
        System.out.println(result);
    }
}

Cuando usa Callable, no puede usar directamente los parámetros del constructor de Thread.

Puedes usar una clase auxiliar: FutureTask 

 Entonces, ¿cuándo se puede calcular este resultado? Este problema se puede resolver utilizando FutureTask:

Similar a cuando vas a comer Malatang, después de pagar el dinero, el jefe te dará un recibo. Cuando el Malatang esté listo, puedes usar el recibo para recoger la comida. Aquí, usas FutureTask para obtener el resultado.


2、Bloqueo reentrante

Bloqueo mutex reentrante.Similar al posicionamiento sincronizado , se utiliza para lograr efectos de exclusión mutua y garantizar la seguridad de los subprocesos.
ReentrantLock también es un bloqueo reentrante. El significado original de la palabra "Reentrant" es "reentrante".
Este bloqueo no se usa tan comúnmente como el sincronizado, pero también es un componente de bloqueo opcional.
Este bloqueo está más cerca del bloqueo en C++ en uso.

bloquear() bloquear desbloquear() desbloquear

ReentrantLock tiene algunas características que sincronizado no tiene:

1. Proporciona un método tryLock para bloquear

Para la operación de bloqueo, si el bloqueo no tiene éxito, se bloqueará y esperará (esperará hasta morir) 

Para trylock, si el bloqueo falla, se devuelve falso directamente / también puede configurar el tiempo de espera

tryLock proporciona más espacio operativo para operaciones de bloqueo

2. ReentrantLock tiene dos modos, que pueden funcionar en estado de bloqueo justo o en estado de bloqueo injusto.

Modo justo/injusto que se puede configurar mediante parámetros en el constructor

3. ReentrantLock también tiene un mecanismo de notificación de espera, que se puede completar con una clase como Condición.

La notificación de espera aquí es más poderosa que la notificación de espera.

Estas son las ventajas de ReentrantLock

Pero la desventaja de ReentranLock también es obvia: el desbloqueo es fácil de pasar por alto; por lo tanto, podemos usar finalmente para ejecutar el desbloqueo.

El objeto de bloqueo sincronizado es cualquier objeto y el objeto de bloqueo ReentrantLock es él mismo.

En otras palabras, si varios subprocesos llaman al método de bloqueo para diferentes ReentrantLocks, no habrá competencia de bloqueo.

En el desarrollo real, cuando se realiza un desarrollo de subprocesos múltiples y se utilizan bloqueos, sincronizado sigue siendo la primera opción. 


3. clase atómica

La mayor parte del contenido se ha discutido antes.

¿Cuáles son los escenarios de aplicación de las clases atómicas?

(1) Requisitos de conteo

Número de jugadas, me gusta, monedas, reenvíos, cobros…

El mismo vídeo tiene a muchas personas reproduciéndolo, dándole me gusta o poniéndolo como favorito al mismo tiempo...

(2) Efecto estadístico

Cuente el número de solicitudes con errores, utilizando la clase atómica

Registre la cantidad de solicitudes erróneas, escriba otro servidor de monitoreo, obtenga estos recuentos de errores del servidor de visualización y dibújelos en la página mediante un gráfico.

Si después del lanzamiento de un programa, la cantidad de errores aumenta repentinamente de manera significativa, significa que existe una alta probabilidad de que esta versión del código contenga errores.


4. Semáforo _

Los semáforos también suelen aparecer en los sistemas operativos.

El semáforo es un concepto/componente importante en la programación concurrente

Para ser precisos, Semaphore es un contador (variable) que describe la cantidad de recursos disponibles y recursos críticos.

Recursos críticos: recursos que pueden ser utilizados comúnmente por múltiples procesos/subprocesos y otras entidades que se ejecutan simultáneamente (varios subprocesos modifican la misma variable, esta variable puede considerarse un recurso crítico)

Describe si el hilo actual tiene recursos críticos disponibles.

En la entrada del estacionamiento suele haber un display: hay XX espacios vacantes en el estacionamiento, donde este dato es el semáforo, y los espacios de estacionamiento vacantes son los recursos disponibles.

Si entro al estacionamiento equivale a solicitar un lugar de estacionamiento (solicitar un recurso disponible), en este momento el contador será -1, lo que se llama operación P, adquirir (solicitud)

Si salgo del estacionamiento, equivale a liberar un espacio de estacionamiento (liberar un recurso disponible), en este momento el contador será +1, lo que se llama operación V, liberar.

Cuando el contador es 0, si continúa realizando la operación P, será bloqueado y esperará hasta que otros subprocesos realicen la operación V y liberen un recurso inactivo.

Este proceso de bloqueo y espera es un poco como un candado.

Un candado es esencialmente un semáforo especial (el valor interior es 0 o 1)

Un semáforo es más general que un candado: puede describir no solo un recurso, sino también N recursos.

Aunque conceptualmente es más amplio, todavía hay más bloqueos en el desarrollo real (el escenario del semáforo binario es más común)


5. CountDownLatch

Un componente para un escenario específico

Por ejemplo: descargar algo

A veces, la descarga de un archivo relativamente grande es relativamente lenta (no debido al límite de velocidad en casa, sino a menudo debido a limitaciones en el servidor)

Hay algunos descargadores de subprocesos múltiples que dividen un archivo grande en varias partes más pequeñas y utilizan varios subprocesos para descargarlos.

Cada hilo es responsable de descargar una parte y cada hilo es una conexión de red.

Puede aumentar considerablemente la velocidad de descarga

Cuando es necesario dividir una tarea en varias tareas para completarla, ¿cómo medir si se han completado varias tareas ahora?

En este momento, puedes usar CountDownLatch 

import java.util.concurrent.CountDownLatch;

//CountDownLatch
public class Demo29 {
    public static void main(String[] args) throws InterruptedException {
        //构造方法中,指定要创建几个任务
        CountDownLatch countDownLach = new CountDownLatch(10);
        for(int i = 0;i < 10;i ++){
            int id = i;
            Thread t = new Thread(() ->{
                System.out.println("线程" + id + " 开始工作");
                try {
                    //使用 sleep 代指某些耗时操作
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程" + id + " 结束工作");
                //在每个任务结束这里,调用一下方法
                //把 10 个线程想象成短跑比赛的 10 个运动员,countDown 就是运动员撞线了
                countDownLach.countDown();
            });
            t.start();
        }

        //主线程如何知道上述所有任务是否都完成了呢?
        //难道要在主线程中调用 10 次 join 吗?
        //万一要是这个任务结束,但是线程不需要结束,join 不也就不行了吗
        //主线程中可以使用 countDownLatch 负责等待任务结束
        // a -> all,等待所有任务结束,当调用 countDown 次数 < 初始设置的次数,await 就会阻塞
        countDownLach.await();
        System.out.println("多个线程的所有任务都执行完毕");
    }
}

11. Clase de colección

Clases de colección, ¿cuáles son seguras para subprocesos?

Vector, Stack y HashTable son seguros para subprocesos (no recomendado), pero otras clases de colección no son seguras para subprocesos.
Estos tres utilizan métodos sincronizados en clave, por lo que se consideran seguros para subprocesos.
Vector y HashTable son clases de colección creadas en los primeros días de Java.
Agregar un candado no significa necesariamente que sea seguro para subprocesos, y no agregar un candado no significa necesariamente que no sea seguro para subprocesos. Para juzgar si un código es seguro para subprocesos, debe analizar los problemas específicos en detalle.
Aunque los métodos get y set de HashTable están sincronizados, pueden ocurrir problemas de seguridad de subprocesos si no se usan correctamente.
(1) Si hay varios subprocesos, la ejecución simultánea de operaciones establecidas es segura para subprocesos debido a restricciones sincronizadas.
(2) Si varios subprocesos realizan operaciones más complejas, como determinar que el valor de get es xxx y luego realizar set, es posible que dichas operaciones no sean seguras para subprocesos.
Por lo tanto, aunque las clases de colección como Vecdtor y Hashtable están sincronizadas, no se puede garantizar que sean seguras para subprocesos. Al mismo tiempo, en el caso de un solo subproceso, la sincronización puede afectar la eficiencia de la ejecución (la eliminación del bloqueo solo puede reducir el impacto). hasta cierto punto, pero no al 100%)

(1) Usar ArrayList en múltiples entornos

1. Utilice el mecanismo de sincronización usted mismo (sincronizado o ReentrantLock)
Se han realizado muchas discusiones relacionadas antes y no las ampliaremos aquí.
2、Collections.synchronizedList (nueva ArrayList);
 Collections.synchronizedList(new ArrayList);

ArrayList en sí no usa sincronizado

Pero si no quieres bloquearlo tú mismo, puedes usar lo anterior

Hacer que ArrayList funcione como Vector (rara vez se usa)

3. Utilice CopyOnWriteArrayList
Copiar en escrito
No es seguro que varios subprocesos modifiquen la misma variable al mismo tiempo, entonces, si varios subprocesos modifican diferentes variables al mismo tiempo, ¿es seguro?

Si se utilizan varios subprocesos para leer, no habrá ningún problema de seguridad de los subprocesos.

Una vez que un hilo lo modifica, hará una copia de sí mismo. 

Especialmente si la modificación lleva mucho tiempo, otros subprocesos seguirán leyendo los datos antiguos.

Una vez completada la modificación, use el nuevo ArrayList para reemplazar el antiguo ArrayList (esencialmente una reasignación de una referencia, extremadamente rápida y atómica)

En este proceso no se introducen operaciones de bloqueo.

Las modificaciones seguras para subprocesos se completan mediante el proceso de crear una copia --> modificar una copia --> usar una copia para reemplazarla.


(2) Entorno multiproceso que utiliza colas

1) ArrayBlockingQueue es una cola de bloqueo implementada en base a matrices
2) LinkedBlockingQueue es una cola de bloqueo implementada según una lista vinculada
3) PriorityBlockingQueue es una cola de bloqueo de prioridad implementada en función del montón
4) TransferQueue es una cola de bloqueo que contiene como máximo un elemento

(3) El entorno multiproceso utiliza una tabla hash

(a) Tabla hash

HashMap no es seguro para subprocesos

HashTable es seguro para subprocesos y los métodos clave están sincronizados.

ConcurrentHashMap es una tabla hash segura para subprocesos
HashTable agrega sincronizado directamente al método, lo que equivale a bloquear esto

Cualquier operación de lectura en el objeto ht implicará bloquear este

En este momento, si hay muchos subprocesos que quieren operar ht, definitivamente se desencadenará una feroz competencia de bloqueo. Al final, estos subprocesos solo se pueden poner en cola uno por uno.
Únase al equipo y ejecútelo en secuencia (el grado de concurrencia es muy bajo)

 En la tabla hash, la clave se calcula mediante la función hash y finalmente se obtiene el subíndice de la matriz. Cuando key1 y key2 finalmente obtienen el número

Cuando los subíndices del grupo son iguales, se produce un conflicto de hash.
El método de detección secundaria generalmente no aparece cuando se utilizan tablas hash reales, por lo que utilizamos el método de lista vinculada para resolver este conflicto hash.
Siempre que se controle la longitud de la lista vinculada y no sea demasiado larga, la complejidad del tiempo sigue siendo O (1).
Si las dos operaciones de modificación son para dos listas enlazadas diferentes, ¿existe algún problema de seguridad del hilo?
¡Obviamente no! !
Si sucede que la operación de inserción actual desencadena la expansión, puede tener un impacto. La operación de expansión es una operación de peso considerable.
Entonces toda la tabla hash se movió nuevamente y, en comparación, la sobrecarga de bloqueo fue mínima.
Dado que no existe ningún problema de seguridad de subprocesos al simplemente modificar dos listas vinculadas, ¿no es necesario agregar este bloqueo?

El método específico es organizar un candado para cada lista vinculada.

Hay muchas listas enlazadas en una tabla hash y la probabilidad de que dos subprocesos estén operando la misma lista enlazada al mismo tiempo es muy baja.

, la sobrecarga general del bloqueo se reduce considerablemente.

Dado que cualquier objeto sincronizado se puede usar para bloquear, simplemente puede usar el nodo principal de cada lista vinculada como objeto de bloqueo.


(b) Mapa concurretnhash

Mejoras en ConcurrentHashMap:

1. La granularidad de los bloqueos se reduce. Cada lista vinculada tiene un bloqueo. En la mayoría de los casos, los conflictos de bloqueo no estarán involucrados [Núcleo]

2. Las operaciones CAS se utilizan ampliamente y size ++ se realizará al mismo tiempo. Este tipo de operaciones no provocarán conflictos de bloqueo.

3. La operación de escritura está bloqueada en el nivel de la lista vinculada, pero la operación de lectura no está bloqueada.

4. Optimizado para operaciones de expansión: expansión progresiva

Una vez que se activa la expansión de HashTable, inmediatamente completará la transferencia de todos los elementos de una sola vez, lo que lleva bastante tiempo.

Expansión progresiva: divídala en partes. Cuando sea necesaria la expansión, se creará otra matriz más grande y luego los datos de la matriz anterior se moverán gradualmente a la nueva matriz. Habrá un período de tiempo en el que la matriz anterior La matriz y la nueva matriz existe al mismo tiempo

1. Agregue nuevos elementos e insértelos en la nueva matriz.

2. Para eliminar elementos, simplemente elimine los elementos antiguos de la matriz.

3. Para encontrar elementos, se debe buscar tanto en la matriz nueva como en la matriz anterior.

4. Modifique el elemento y colóquelo en la nueva matriz de manera uniforme.

Al mismo tiempo, cada operación desencadenará un cierto grado de transferencia. Cada vez que transfiera un poco, puede asegurarse de que el tiempo total no sea muy largo. Después de que se sume un poco, la transferencia se completará gradualmente y el La matriz antigua puede destruirse por completo.

Antes de Java8, ConcurrentHashMap usaba bloqueos segmentados. A partir de Java8, hay un bloqueo para cada lista vinculada.

Los bloqueos segmentados pueden mejorar la eficiencia, pero no son tan buenos como un bloqueo para cada lista vinculada y el código es más complejo de implementar. 

Supongo que te gusta

Origin blog.csdn.net/weixin_73616913/article/details/132087954
Recomendado
Clasificación