Ensayo de estereotipos concurrentes 2023: preguntas de la entrevista

conocimiento básico

Ventajas y desventajas de la programación concurrente Por qué utilizar la programación concurrente (ventajas de la programación concurrente)

  • Aproveche al máximo la potencia informática de las CPU de varios núcleos: mediante la programación concurrente, se puede maximizar la potencia informática de las CPU de varios núcleos y mejorar el rendimiento.
  • Facilite la división de negocios y mejore la concurrencia y el rendimiento del sistema: en escenarios comerciales especiales, es intrínsecamente adecuado para la programación concurrente. El sistema actual requiere millones o incluso decenas de millones de concurrencia en cada paso, y la programación concurrente de subprocesos múltiples es la base para desarrollar un sistema de alta concurrencia. Hacer un buen uso del mecanismo de subprocesos múltiples puede mejorar en gran medida la concurrencia y el rendimiento generales. del sistema. Frente a modelos comerciales complejos, los programas paralelos son más adecuados para las necesidades comerciales que los programas en serie, y la programación concurrente está más en línea con esta división comercial.

¿Cuáles son las desventajas de la programación concurrente?

El propósito de la programación concurrente es mejorar la eficiencia de ejecución del programa y aumentar la velocidad de ejecución del programa, pero la programación concurrente no siempre mejora la velocidad de ejecución del programa y la programación concurrente puede encontrar muchos problemas, como

: Pérdidas de memoria, cambio de contexto, seguridad de subprocesos, interbloqueos, etc.

¿Cuáles son los tres elementos de la programación concurrente? ¿Cómo garantizar la seguridad de la operación multiproceso en programas Java?

Tres elementos de programación concurrente (los problemas de seguridad de subprocesos se reflejan en):

Atomicidad: Un átomo es una partícula que no se puede dividir más. Atomicidad significa que una o más operaciones tienen éxito o fracasan.

Visibilidad: cuando un hilo modifica una variable compartida, otro hilo puede verla inmediatamente. (sincronizado, volátil)

Orden: el orden en que se ejecuta el programa se ejecuta en el orden en que se ejecutan los códigos. (El procesador puede reordenar las instrucciones)

Razones de los problemas de seguridad de los subprocesos:

  • Problemas de atomicidad causados ​​por el cambio de subprocesos.
  • Problemas de visibilidad causados ​​por el almacenamiento en caché
  • El problema de orden causado por la optimización de la compilación.

Solución:

  • Las clases atómicas, sincronizadas y LOCK al comienzo de JDK Atomic pueden resolver problemas atómicos
  • sincronizado, volátil, LOCK, puede resolver el problema de visibilidad
  • La regla Sucede antes puede resolver el problema del orden

¿Cuál es la diferencia entre paralelo y concurrente?

  • Concurrencia: se ejecutan varias tareas en el mismo núcleo de CPU por turnos (alternativamente) según intervalos de tiempo subdivididos. Desde un punto de vista lógico, esas tareas se ejecutan al mismo tiempo.
  • Paralelismo: en una unidad de tiempo, varios procesadores o procesadores de múltiples núcleos procesan múltiples tareas al mismo tiempo, lo cual es "simultáneo" en el verdadero sentido.
  • Serie: hay n tareas, que se ejecutan secuencialmente por un hilo. Dado que las tareas y los métodos se ejecutan en un hilo, no hay inseguridad en el hilo y no hay problemas de sección crítica.

Para hacer una metáfora de imagen:

Concurrencia = dos colas y una máquina de café.
Paralelo = dos colas y dos cafeteras.
Serie = una cola y una máquina de café.

¿Qué es el multiproceso, las ventajas y desventajas del multiproceso?

Subprocesos múltiples: subprocesos múltiples significa que un programa contiene múltiples flujos de ejecución, es decir, se pueden ejecutar múltiples subprocesos diferentes simultáneamente en un programa para realizar diferentes tareas.

Beneficios del subproceso múltiple: puede mejorar la utilización de la CPU. En un programa de subprocesos múltiples, cuando un subproceso debe esperar, la CPU puede ejecutar otros subprocesos en lugar de esperar, lo que mejora enormemente la eficiencia del programa. Es decir, un único programa puede crear múltiples subprocesos de ejecución paralela para completar sus respectivas tareas.

Desventajas del subproceso múltiple:

  • Los subprocesos también son programas, por lo que los subprocesos necesitan ocupar memoria y más subprocesos ocupan más memoria;
  • Los subprocesos múltiples requieren coordinación y gestión, por lo que se requiere tiempo de CPU para realizar un seguimiento de los subprocesos;
  • El acceso a recursos compartidos entre subprocesos se afectará entre sí y debe resolverse el problema de competir por recursos compartidos.

¿Cuál es la diferencia entre un hilo y un proceso?

**proceso**

Una aplicación que se ejecuta en la memoria. Cada proceso tiene su propio espacio de memoria independiente y un proceso puede tener varios subprocesos. Por ejemplo, en un sistema Windows, un xx.exe en ejecución es un proceso.

hilo

Una tarea de ejecución (unidad de control) en un proceso que es responsable de la ejecución de programas en el proceso actual. Un proceso tiene al menos un subproceso, un proceso puede ejecutar varios subprocesos y varios subprocesos pueden compartir datos.

**La diferencia entre proceso e hilo**

Los hilos tienen muchas características de los procesos tradicionales, por lo que también se les llama procesos livianos o elementos de proceso; mientras que los procesos tradicionales se llaman procesos pesados, equivale a una tarea con un solo hilo. En un sistema operativo que introduce subprocesos, normalmente un proceso tiene varios subprocesos, incluido al menos uno.

**Diferencia fundamental:** El proceso es la unidad básica de asignación de recursos del sistema operativo, mientras que el subproceso es la unidad básica de programación y ejecución de tareas del procesador.

** Gastos generales de recursos: ** Cada proceso tiene su propio código y espacio de datos (contexto del programa), cambiar entre programas tendrá una gran sobrecarga; los subprocesos pueden considerarse procesos livianos y los subprocesos del mismo tipo comparten código y espacio de datos. Cada subproceso tiene su propia pila en ejecución y contador de programa (PC) independientes, y la sobrecarga de cambiar entre subprocesos es pequeña.

**Relación de inclusión:** Si hay varios subprocesos en un proceso, el proceso de ejecución no es una línea, sino que se completan varias líneas (subprocesos) juntas; los subprocesos son parte del proceso, por lo que los subprocesos también se denominan procesos livianos o livianos. proceso.

**Asignación de memoria:** Los subprocesos del mismo proceso comparten el espacio de direcciones y los recursos del proceso, mientras que los espacios de direcciones y los recursos entre procesos son independientes entre sí.

**Relación de influencia:** Después de que un proceso falla, no afectará a otros procesos en modo protegido, pero si un subproceso falla, todo el proceso morirá. Por tanto, el multiprocesamiento es más sólido que el multiproceso.

**Proceso de ejecución:**Cada proceso independiente tiene una entrada de ejecución del programa, una secuencia de ejecución secuencial y una salida del programa. Sin embargo, los subprocesos no se pueden ejecutar de forma independiente y deben depender del programa de aplicación. El programa de aplicación proporciona control de ejecución de múltiples subprocesos y ambos se pueden ejecutar al mismo tiempo.

¿Qué es el cambio de contexto?

En la programación de subprocesos múltiples, el número de subprocesos es generalmente mayor que el número de núcleos de CPU, y un núcleo de CPU solo puede ser utilizado por un subproceso a la vez. Para permitir que estos subprocesos se ejecuten de manera efectiva, la estrategia adoptada por la CPU debe proporcionar a cada subproceso la forma de asignar intervalos de tiempo y rotarlos. Cuando se agota el intervalo de tiempo de un subproceso, estará listo nuevamente para que lo utilicen otros subprocesos. Este proceso es un cambio de contexto.

En pocas palabras: la tarea actual guardará su estado antes de cambiar a otra tarea después de ejecutar el intervalo de tiempo de la CPU, de modo que el estado de esta tarea se pueda cargar nuevamente la próxima vez. El proceso desde guardar hasta recargar una tarea es un cambio de contexto.

El cambio de contexto suele ser un proceso computacional intensivo. Es decir, requiere una cantidad considerable de tiempo de procesador, y entre decenas o cientos de veces de conmutación por segundo, cada conmutación lleva un tiempo del orden de nanosegundos. Por lo tanto, el cambio de contexto significa consumir mucho tiempo de CPU para el sistema; de hecho, puede ser la operación que consume más tiempo en el sistema operativo.

Una de las muchas ventajas que tiene Linux sobre otros sistemas operativos (incluidos otros sistemas similares a Unix) es que el cambio de contexto y el cambio de modo requieren mucho tiempo.

¿Cuál es la diferencia entre un hilo de demonio y un hilo de usuario?

Hilos de demonio y hilos de usuario

  • Subproceso de usuario (Usuario): ejecutarse en primer plano, realizar tareas específicas, como el subproceso principal del programa, subprocesos conectados a la red, etc., son todos subprocesos de usuario.
  • Hilo de demonio: se ejecuta en segundo plano y sirve a otros hilos en primer plano. También se puede decir que el hilo del demonio es el "sirviente" del hilo que no es el demonio en la JVM. Una vez que todos los subprocesos del usuario hayan terminado de ejecutarse, el subproceso del demonio terminará de funcionar con la JVM.

El subproceso donde se encuentra la función principal es un subproceso de usuario. Cuando se inicia la función principal, también se inician muchos subprocesos de demonio dentro de la JVM, como los subprocesos de recolección de basura. Una de las diferencias más obvias es que cuando finaliza el hilo del usuario, la JVM sale, independientemente de si hay un hilo de demonio ejecutándose en ese momento. El hilo del demonio no afectará la salida de la JVM.

Precauciones:

  1. setDaemon(true) debe ejecutarse antes del método start(); de lo contrario, se generará una excepción IllegalThreadStateException.
  2. Un nuevo hilo generado en un hilo de demonio también es un hilo de demonio
  3. No todas las tareas se pueden asignar a subprocesos de demonio para su ejecución, como operaciones de lectura y escritura o lógica de cálculo.
  4. No se puede confiar en el contenido del bloque finalmente en un subproceso Daemon para garantizar que se ejecute la lógica para cerrar o limpiar recursos. Porque también dijimos anteriormente que una vez que todos los subprocesos del usuario terminen de ejecutarse, el subproceso del demonio terminará de funcionar junto con la JVM, por lo que es posible que el bloque de declaración finalmente en el subproceso del demonio no se ejecute.

¿Cómo encontrar qué subproceso tiene la mayor utilización de CPU en Windows y Linux?

Utilice el administrador de tareas en Windows para verlo y utilice la herramienta superior para verlo en Linux.

  1. Encuentre el pid del proceso que consume más CPU, ejecute el comando superior en la terminal y luego presione shift+p para encontrar el número de pid del proceso que consume más CPU
  2. Según el número de pid obtenido en el primer paso anterior, top -H -p pid. Luego presione shift+p para averiguar el número de subprocesos con alta utilización de CPU, como top -H -p 1328
  3. Convierta el número de hilo obtenido a hexadecimal, simplemente vaya a Baidu para convertirlo
  4. Utilice la herramienta jstack para imprimir la información del proceso, número de pid de jstack> /tmp/t.dat, como jstack 31365> /tmp/t.dat
  5. Edite el archivo /tmp/t.dat para encontrar la información correspondiente al número de hilo

¿Qué es el punto muerto del hilo?

Enciclopedia Baidu: Deadlock se refiere a un fenómeno de bloqueo causado por dos o más procesos (hilos) durante la ejecución debido a la competencia por recursos o la comunicación entre sí, sin fuerza externa no podrán avanzar. En este momento, se dice que el sistema está en un estado de punto muerto o que el sistema está en un punto muerto, y estos procesos (subprocesos) que siempre están esperando entre sí se denominan procesos de punto muerto (hilos).

Se bloquean varios subprocesos al mismo tiempo, uno o todos están esperando que se libere un recurso. Dado que el hilo está bloqueado indefinidamente, es imposible que el programa finalice correctamente.

Como se muestra en la figura siguiente, el hilo A contiene el recurso 2 y el hilo B contiene el recurso 1. Ambos quieren solicitar los recursos del otro al mismo tiempo, por lo que los dos hilos se esperarán entre sí y entrarán en un estado de punto muerto.

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo de enlace antirrobo, se recomienda guardar la imagen y cargarla directamente (img-gWfnTdZi-1692509038138) (04-Preguntas de la entrevista de programación concurrente (última versión de 2020) - focus.assets/hilo punto muerto .png)]

Interbloqueo de subprocesos El siguiente es un ejemplo para ilustrar el interbloqueo de subprocesos. El código simula la situación de interbloqueo en la figura anterior:

1 public class DeadLockDemo { 
2  private static Object resource1 = new Object();//资源 1 
3  private static Object resource2 = new Object();//资源 2
4 
5  public static void main(String[] args) {
6  new Thread(() ‐> { 
7  synchronized (resource1) { 
8  System.out.println(Thread.currentThread() + "get resource1"); 
9  try { 
10  Thread.sleep(1000); 
11  } catch (InterruptedException e) { 
12  e.printStackTrace(); 
13  } 
14  System.out.println(Thread.currentThread() + "waiting get resource2"); 
15  synchronized (resource2) { 
16  System.out.println(Thread.currentThread() + "get resource2"); 
17  } 
18  } 
19  }, "线程 1").start();
20 
21  new Thread(() ‐> { 
22  synchronized (resource2) { 
23  System.out.println(Thread.currentThread() + "get resource2"); 
24  try { 
25  Thread.sleep(1000); 
26  } catch (InterruptedException e) { 
27  e.printStackTrace(); 
28  } 
29  System.out.println(Thread.currentThread() + "waiting get resource1"); 
30  synchronized (resource1) { 
31  System.out.println(Thread.currentThread() + "get resource1"); 
32  } 
33  } 
34  }, "线程 2").start(); 
35  } 
36 } 

resultado de salida

1 Thread[线程 1,5,main]get resource1 
2 Thread[线程 2,5,main]get resource2 
3 Thread[线程 1,5,main]waiting get resource2 
4 Thread[线程 2,5,main]waiting get resource1 

El subproceso A obtiene el bloqueo del monitor del recurso1 a través de sincronizado (recurso1) y luego pasa Thread.sleep(1000); deja que el subproceso A duerma durante 1 segundo para permitir que el subproceso B obtenga el derecho de ejecución de la CPU y luego obtenga el bloqueo del monitor del recurso2. . Una vez finalizado el sueño del hilo A y el hilo B, ambos comienzan a solicitar los recursos de la otra parte, y luego los dos hilos caerán en un estado de espera el uno por el otro, lo que provocará un punto muerto. El ejemplo anterior cumple las cuatro condiciones necesarias para que se produzca un punto muerto.

¿Cuáles son las cuatro condiciones necesarias para que se forme un punto muerto?

  1. Condiciones de exclusión mutua: los subprocesos (procesos) son exclusivos de los recursos asignados, es decir, un recurso solo puede ser ocupado por un subproceso (proceso) hasta que sea liberado por el subproceso (proceso)
  2. Condiciones de solicitud y retención: cuando un hilo (proceso) se bloquea debido a una solicitud de recursos ocupados, no soltará los recursos obtenidos.
  3. Condición de no privación: los recursos obtenidos por un subproceso (proceso) no pueden ser privados por la fuerza por otros subprocesos antes de que se agoten, y los recursos se liberan solo después de que se agoten.
  4. Condición de espera circular: cuando ocurre un punto muerto, el hilo (proceso) en espera debe formar un bucle (similar a un bucle infinito), lo que resulta en un bloqueo permanente.

Cómo evitar el punto muerto del hilo

Sólo necesitamos destruir una de las cuatro condiciones que provocan el estancamiento.

romper la condición mutex

No tenemos forma de destruir esta condición porque usamos bloqueos para hacerlos mutuamente excluyentes (los recursos críticos requieren acceso mutuamente exclusivo).

Solicitud de destrucción y condición de retención

Solicite todos los recursos a la vez.

**Romper la condición de no privación**

Cuando un hilo que ocupa algunos recursos solicita otros recursos, si no se puede obtener la aplicación, puede liberar activamente los recursos que ocupa.

**Condición de espera de interrupción del bucle**

Prevenir solicitando recursos en secuencia. Solicite recursos en un orden determinado y libere recursos en orden inverso. Destruye la condición de espera del bucle.

Modificamos el código del hilo 2 a lo siguiente para que no se produzca ningún punto muerto.

1 new Thread(() ‐> { 
2  synchronized (resource1) { 
3  System.out.println(Thread.currentThread() + "get resource1"); 
4  try { 
5  Thread.sleep(1000); 
6  } catch (InterruptedException e) { 
7  e.printStackTrace();
8  } 
9  System.out.println(Thread.currentThread() + "waiting get resource2"); 
10  synchronized (resource2) { 
11  System.out.println(Thread.currentThread() + "get resource2"); 
12  } 
13  } 
14 }, "线程 2").start(); 

resultado de salida

1 Thread[线程 1,5,main]get resource1 
2 Thread[线程 1,5,main]waiting get resource2 
3 Thread[线程 1,5,main]get resource2 
4 Thread[线程 2,5,main]get resource1 
5 Thread[线程 2,5,main]waiting get resource2 
6 Thread[线程 2,5,main]get resource2 

Analicemos por qué el código anterior evita puntos muertos.

El subproceso 1 primero obtiene el bloqueo del monitor del recurso1 y el subproceso 2 no puede obtenerlo en este momento. Luego, el subproceso 1 adquiere el bloqueo del monitor del recurso2, que se puede adquirir. Luego, el subproceso 1 libera la ocupación de los bloqueos del monitor de recurso1 y recurso2, y el subproceso 2 puede ejecutarlos después de adquirirlos. Esto rompe la condición de espera del bucle de interrupción, evitando así el punto muerto.

Cuatro formas de crear hilos.

¿Cuáles son las formas de crear hilos?

Hay cuatro formas de crear hilos:

  • Heredar la clase Thread;
  • Implementar la interfaz Runnable;
  • Implementar la interfaz invocable;
  • Utilice la clase de herramienta Executors para crear un grupo de subprocesos y heredar la clase Thread

paso

  1. Defina una subclase de la clase Thread, reescriba el método de ejecución e implemente la lógica relevante, el método run()

Es el método de lógica de negocios que ejecutará el hilo.

  1. Crear un objeto de subclase de hilo personalizado

  2. Llame al método star() de la instancia de la subclase para iniciar el hilo

1 public class MyThread extends Thread {
2 
3	@Override
4	public void run() {
5	System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
6	}
7
8 }
1 public class TheadTest {
2
3	public static void main(String[] args) {
4	MyThread myThread = new MyThread();
5	myThread.start();
6	System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
7	}
8
9 }
10

resultado de la operación

1 main main()方法执行结束 
2 Thread‐0 run()方法正在执行... 

Implementar la interfaz ejecutable

paso

  1. Defina la interfaz Runnable para implementar la clase MyRunnable y anule el método run()

  2. Cree una instancia de MyRunnable myRunnable, cree un objeto Thead con myRunnable como objetivo, el objeto Thread es el objeto de hilo real

  3. Llame al método start() del objeto hilo.

1 public class MyRunnable implements Runnable {
2
3	@Override
4	public void run() {
5	System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
6	}
7
8 }
1 public class RunnableTest {
2
3	public static void main(String[] args) {
4	MyRunnable myRunnable = new MyRunnable();
5	Thread thread = new Thread(myRunnable);
6	thread.start();
7	System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
8	}
9
10 }

Resultados de la

1	main main()方法执行完成
2	Thread‐0 run()方法执行中...

Implementar la interfaz invocable

paso

  1. Cree una clase myCallable que implemente la interfaz Callable

  2. Cree un objeto FutureTask con myCallable como parámetro

  3. Cree un objeto Thread con FutureTask como parámetro

  4. Llame al método start() del objeto hilo.

1 public class MyCallable implements Callable<Integer> {
2
3	@Override
4	public Integer call() {
5	System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
6	return 1;
7	}
8
9 }
1 public class CallableTest {
2
3	public static void main(String[] args) {
4	FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
5	Thread thread = new Thread(futureTask);
6	thread.start();
7
8	try {
9	Thread.sleep(1000);
10	System.out.println("返回结果 " + futureTask.get());
11	} catch (InterruptedException e) {
12	e.printStackTrace();
13	} catch (ExecutionException e) {
14	e.printStackTrace();
15	}
16	System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
17	}
18
19 }

Resultados de la

1	Thread‐0 call()方法执行中...
2	返回结果 1
3	main main()方法执行完成

Cree un grupo de subprocesos utilizando la clase de herramienta Ejecutores

Los ejecutores proporcionan una serie de métodos de fábrica para crear grupos de subprocesos, y todos los grupos de subprocesos devueltos implementan la interfaz ExecutorService. Existen principalmente newFixedThreadPool, newCachedThreadPool, newSingleThreadExecutor, newScheduledThreadPool, y estos cuatro grupos de subprocesos se presentarán en detalle más adelante.

1 public class MyRunnable implements Runnable {
2
3	@Override
4	public void run() {
5	System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
6	}
7
8 }
1 public class SingleThreadExecutorTest {
2 
3  public static void main(String[] args) { 
4  ExecutorService executorService = Executors.newSingleThreadExecutor(); 
5  MyRunnable runnableTest = new MyRunnable(); 
6	for (int i = 0; i < 5; i++) {
7	executorService.execute(runnableTest);
8	}
9
10	System.out.println("线程任务开始执行");
11	executorService.shutdown();
12	}
13
14 }

Resultados de la

1	线程任务开始执行
2	pool‐1‐thread‐1 is running...
3	pool‐1‐thread‐1 is running...
4	pool‐1‐thread‐1 is running...
5	pool‐1‐thread‐1 is running...
6	pool‐1‐thread‐1 is running...

¿Cuál es la diferencia entre ejecutar() e iniciar() de un hilo?

Cada hilo completa su operación a través del método run () correspondiente a un objeto Thread específico, y el método run () se llama cuerpo del hilo. Inicie un hilo llamando al método start() de la clase Thread. El método start() se utiliza para iniciar el hilo y el método run() se utiliza para ejecutar el código de tiempo de ejecución del hilo. run() se puede llamar repetidamente, mientras que start() solo se puede llamar una vez. El método start () se utiliza para iniciar un subproceso, que realmente realiza operaciones de subprocesos múltiples. Llamar al método start () no necesita esperar a que se complete la ejecución del código del cuerpo del método de ejecución y puede continuar ejecutando otros códigos directamente; en este momento, el subproceso está en el estado listo y no se está ejecutando. Luego llame al método run () a través de esta clase Thread para completar su estado de ejecución, el método run () finaliza y el hilo finaliza. Luego la CPU programa otros subprocesos.

El método run() está en este hilo, solo una función en el hilo, no multiproceso. Si llama a run () directamente, en realidad es equivalente a llamar a una función ordinaria. El método run () debe esperar a que se ejecute el método run () antes de ejecutar el siguiente código, por lo que todavía hay una sola ruta de ejecución, y no hay ningún subproceso en absoluto, por lo tanto, utilice el método start() en lugar del método run() cuando se ejecute con subprocesos múltiples.

¿Por qué se ejecuta el método run() cuando llamamos al método start() y por qué no podemos llamar al método run() directamente?

Esta es otra pregunta muy clásica de la entrevista sobre subprocesos múltiples de Java y se hace a menudo en las entrevistas. ¡Es muy simple, pero mucha gente no podrá responderla!

nuevo Un hilo, el hilo entra en el nuevo estado. Llamar al método start() iniciará un subproceso y hará que el subproceso entre en el estado listo y pueda comenzar a ejecutarse después de que se asigne el intervalo de tiempo. start () realizará el trabajo de preparación correspondiente del subproceso y luego ejecutará automáticamente el contenido del método run (), que es un trabajo real de subprocesos múltiples.

La ejecución directa del método run () ejecutará el método de ejecución como un método ordinario en el subproceso principal y no lo ejecutará en un subproceso determinado, por lo que este no es un trabajo de subprocesos múltiples.

Resumen: llamar al método de inicio puede iniciar el hilo y hacer que el hilo entre en el estado listo, mientras que el método de ejecución es solo una llamada al método normal del hilo y aún se ejecuta en el hilo principal.

¿Qué son exigibles y futuros?

La interfaz Callable es similar a Runnable, como puede verse por el nombre, pero Runnable no devolverá un resultado y no puede lanzar una excepción que devuelva un resultado, mientras que Callable es más poderoso. Después de ser ejecutado por un hilo, puede regresar un valor Este valor de retorno puede ser obtenido por Future, es decir, Future puede obtener el valor de retorno de tareas ejecutadas de forma asincrónica.

La interfaz Future representa una tarea asincrónica, el resultado de una tarea asincrónica que puede que aún no esté completa. entonces

Callable se usa para producir resultados y Future se usa para obtener resultados.

¿Qué es FutureTask?

FutureTask representa una tarea que opera de forma asincrónica. Se puede pasar una FutureTask

La clase de implementación específica de Callable, que puede esperar para obtener y juzgar el resultado de esta tarea de operación asincrónica.

Compruebe si se ha completado, cancele la tarea y otras operaciones. El resultado solo se puede recuperar cuando se completa la operación y el método get se bloqueará si la operación no se ha completado. Un objeto FutureTask puede llamar

Los objetos invocables y ejecutables están empaquetados. Dado que FutureTask también es una clase de implementación de la interfaz Runnable, FutureTask también se puede colocar en el grupo de subprocesos.

Estado del hilo y operaciones básicas ¿Hablar sobre el ciclo de vida de los hilos y los cinco estados básicos?

[Falló la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-H96OwJqP-1692509038139) (04-Preguntas de la entrevista de programación concurrente (última versión de 2020) -focus .assets/estado del hilo.png)]

  1. Nuevo (nuevo): se crea un objeto de hilo recientemente.

  2. Ejecutable: después de crear el objeto de subproceso, cuando se llama al método start () del objeto de subproceso, el subproceso está en el estado listo, esperando ser seleccionado por la programación del subproceso para obtener el derecho a usar la CPU.

  3. En ejecución (en ejecución): el subproceso en el estado ejecutable (ejecutable) ha obtenido el intervalo de tiempo de la CPU

(timeslice), ejecute el código del programa. Nota: El estado listo es la única entrada para ingresar al estado de ejecución, es decir, si un hilo quiere ingresar al estado de ejecución para su ejecución, primero debe estar en el estado listo;

  1. Bloqueo (bloqueo): por alguna razón, el hilo en el estado de ejecución renuncia temporalmente al derecho de usar la CPU y deja de ejecutarse, en este momento ingresa al estado de bloqueo hasta que ingresa al estado listo antes de tener la oportunidad de ser llamado por la CPU nuevamente para ingresar al estado operativo.

Hay tres tipos de bloqueo:

(1) Esperando bloqueo: el hilo en el estado de ejecución ejecuta el método de espera (), y la JVM colocará el hilo en la cola de espera (cola de espera), de modo que el hilo entre en el estado de bloqueo de espera;

(2) Bloqueo sincrónico: si el subproceso no puede adquirir el bloqueo de sincronización sincronizado (porque el bloqueo está ocupado por otros subprocesos), la JVM colocará el subproceso en el grupo de bloqueo (grupo de bloqueo) y el subproceso ingresará al sincrónico. estado de bloqueo;

(3) Otro bloqueo: al llamar a sleep() o join() del hilo o cuando se emite una solicitud de E/S, el hilo

a un estado bloqueado. Cuando el estado de suspensión () se agota, join () espera a que el subproceso finalice o se agote el tiempo de espera, o cuando se completa el procesamiento de E/S, el subproceso se transfiere nuevamente al estado listo.

  1. Muerto: La ejecución de los métodos run() y main() del hilo finaliza, o el método run() sale debido a una excepción, el hilo finaliza su ciclo de vida. Un hilo muerto no puede volver a resucitar.

¿Cuál es el algoritmo de programación de subprocesos utilizado en Java?

Una computadora generalmente tiene solo una CPU y solo puede ejecutar una instrucción de máquina a la vez, y cada subproceso solo puede ejecutar instrucciones si obtiene el derecho a usar la CPU. La llamada operación concurrente de subprocesos múltiples en realidad significa que desde una perspectiva macro, cada subproceso obtiene el derecho a usar la CPU por turno y realiza sus propias tareas respectivamente. En el grupo en ejecución, habrá varios subprocesos en estado listo esperando la CPU. Una de las tareas de la máquina virtual JAVA es ser responsable de la programación de subprocesos. La programación de subprocesos se refiere a asignar derechos de uso de la CPU a múltiples subprocesos de acuerdo con un mecanismo específico.

Hay dos modelos de programación: el modelo de programación de tiempo compartido y el modelo de programación preventiva.

El modelo de programación de tiempo compartido significa que todos los subprocesos obtienen el derecho a usar la CPU por turno, y el intervalo de tiempo de la CPU ocupado por cada subproceso se asigna de manera uniforme, lo que también es relativamente fácil de entender.

La máquina virtual ava adopta el modelo de programación preventiva, lo que significa que los subprocesos con alta prioridad en el grupo ejecutable tienen prioridad para ocupar

CPU, si los subprocesos en el grupo ejecutable tienen la misma prioridad, entonces se selecciona aleatoriamente un subproceso para ocupar

UPC. Un subproceso en estado de ejecución continúa ejecutándose hasta que tiene que ceder la CPU.

Política de programación de subprocesos

El programador de subprocesos selecciona un subproceso con una prioridad más alta para ejecutar, pero si ocurren las siguientes condiciones, el subproceso finalizará:

(1) Se llama al método de rendimiento en el cuerpo del hilo para ceder el derecho a ocupar la CPU.

(2) El método de suspensión se llama en el cuerpo del hilo para hacer que el hilo entre en suspensión.

(3) El hilo está bloqueado debido a operaciones de IO

(4) Aparece otro hilo de mayor prioridad.

(5) En un sistema que admite intervalos de tiempo, el intervalo de tiempo del hilo se agota

¿Qué son el programador de subprocesos y el corte de tiempo?

El programador de subprocesos es un servicio del sistema operativo que se encarga de asignar tiempo de CPU a los subprocesos en estado Ejecutable. Una vez que creamos un hilo y lo iniciamos, su ejecución depende de la implementación del programador de hilos. La división de tiempo es el proceso de asignar tiempo de CPU disponible a los subprocesos ejecutables disponibles. distribuir

El tiempo de CPU puede basarse en la prioridad del subproceso o en la cantidad de tiempo que espera un subproceso.

La programación de subprocesos no está controlada por la máquina virtual Java, por lo que es mejor que la aplicación la controle.

(es decir, no haga que su programa dependa de la prioridad del subproceso).

Nombre los métodos relacionados con la sincronización y programación de subprocesos.

(1) esperar (): crea un hilo en estado de espera (bloqueo) y libera el bloqueo del objeto retenido;

(2) dormir (): hace que un hilo en ejecución duerma, es un método estático, llame a este método para manejar la excepción InterruptedException;

(3) Notificar (): despierta un hilo en estado de espera. Por supuesto, al llamar a este método, no es posible despertar un determinado hilo en estado de espera, pero la JVM determina qué hilo despertar y no tiene nada que ver con la prioridad;

(4) notityAll(): despierta todos los hilos en espera, este método no otorga el bloqueo del objeto a

todos los subprocesos, pero déjelos competir, y solo el subproceso que adquiere el bloqueo puede entrar en el estado listo;

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

Ambos pueden suspender la ejecución de un hilo.

  • La diferencia entre clases: dormir () es un método estático de la clase de subproceso Thread y esperar () es un método de la clase Objeto.
  • Ya sea para liberar el bloqueo: dormir () no libera el bloqueo; esperar () libera el bloqueo.
  • Diferentes propósitos: la espera se usa generalmente para la interacción/comunicación entre subprocesos y la suspensión generalmente se usa para suspender la ejecución.
  • El uso es diferente: después de llamar al método wait (), el subproceso no se activará automáticamente y otros subprocesos deben llamar al método notify () o notifyAll () en el mismo objeto. Después de ejecutar el método sleep(), el hilo se activará automáticamente. O puede usar esperar (tiempo de espera prolongado) para activar el hilo automáticamente después del tiempo de espera.

¿Cómo llamas al método wait()? ¿Usar if bloque o bucle? ¿Por qué? Los subprocesos en estado de espera pueden recibir falsas alarmas y reactivaciones espurias, y si la condición de espera no se verifica en un bucle, el programa saldrá sin que se cumpla la condición final.

El método wait() debe llamarse en un bucle, porque cuando el subproceso hace que la CPU comience a ejecutarse, es posible que no se hayan cumplido otras condiciones, por lo que sería mejor verificar si se cumplen las condiciones antes del procesamiento. Aquí hay un fragmento de código estándar que utiliza los métodos de espera y notificación:

1	synchronized (monitor) {
2	// 判断条件谓词是否得到满足
3	while(!locked) {
4	// 等待唤醒
5	monitor.wait();
6	}
7	// 处理其他的业务逻辑
8	}

¿Por qué los métodos de comunicación de subprocesos wait(), notify() y notifyAll() están definidos en la clase Object?

En Java, cualquier objeto se puede utilizar como bloqueo, y esperar (), notificar () y otros métodos se utilizan para esperar el bloqueo del objeto o activar el hilo. No hay ningún bloqueo disponible para ningún objeto en el hilo de Java. entonces cualquier objeto llama al método. Debe estar definido en la clase Objeto.

Los métodos wait(), notify() y notifyAll() se llaman en un bloque de código sincrónico

Algunas personas dirán que dado que el hilo abandona el bloqueo del objeto, esperar () también se puede definir en la clase Thread. El hilo recién definido hereda de la clase Thread y no es necesario redefinir la implementación de wait (). método. Sin embargo, hay un problema muy grande con esto: un hilo puede contener muchos candados. Cuando cedes un candado, ¿a qué candado deberías renunciar? Por supuesto, este diseño no es imposible, pero sí más complicado de gestionar.

En resumen, los métodos wait(), notify() y notifyAll() deben definirse en la clase Object.

¿Por qué se deben llamar a wait(), notify() y notifyAll() dentro de un método o bloque sincronizado?

Cuando un hilo necesita llamar al método wait() de un objeto, el hilo debe poseer el bloqueo del objeto.

Luego liberará el bloqueo del objeto y entrará en estado de espera hasta que otros subprocesos llamen al método en este objeto.

método notificar(). De manera similar, cuando un hilo necesita llamar al método notify() del objeto, libera el

Este objeto se bloquea, para que otros subprocesos en espera puedan obtener este bloqueo de objeto. Dado que todos estos métodos requieren que el hilo mantenga el bloqueo del objeto, solo se pueden implementar mediante sincronización, por lo que solo se pueden llamar en un método sincronizado o en un bloque sincronizado.

¿Qué hace el método de rendimiento en la clase Thread?

Haga que el hilo actual cambie del estado de ejecución (estado en ejecución) al estado ejecutable (estado listo).

El subproceso actual ha alcanzado el estado listo, entonces, ¿qué subproceso cambiará del estado listo al estado de ejecución a continuación? Puede ser el hilo actual u otros hilos, dependiendo de la asignación del sistema.

¿Por qué los métodos sleep() y yield() de la clase Thread son estáticos?

Los métodos sleep() y yield() de la clase Thread se ejecutarán en el hilo que se está ejecutando actualmente. entonces en su

No tiene sentido llamar a estos métodos en un hilo que está en estado de espera. Por eso estos métodos son estáticos. Pueden funcionar en el subproceso que se está ejecutando actualmente y evitar que los programadores piensen erróneamente que pueden ser llamados desde otros subprocesos que no se están ejecutando.

¿Cuál es la diferencia entre el método sleep() y el método yield() de un hilo?

(1) El método sleep() no considera la prioridad del hilo cuando da a otros hilos la oportunidad de ejecutarse, por lo que dará la oportunidad de ejecutarse a los hilos de baja prioridad; el método yield() solo dará a los hilos con la misma prioridad o superior prioridad para ejecutar la oportunidad;

(2) El hilo ingresa al estado bloqueado después de ejecutar el método sleep () y ingresa al estado listo después de ejecutar el método yield ();

(3) El método sleep() declara lanzar InterruptedException, mientras que el método yield() no declara ninguna excepción;

(4) El método dormir () es más portátil que el método rendimiento () (relacionado con la programación de la CPU del sistema operativo) y, en general, no se recomienda utilizar el método rendimiento () para controlar la ejecución de subprocesos concurrentes.

¿Cómo detener un hilo en ejecución?

Existen las siguientes 3 formas de finalizar un hilo en ejecución en Java:

\ 1. Utilice el indicador de salida para que el hilo salga normalmente, es decir, el hilo termina cuando se completa el método de ejecución.

\ 2. Utilice el método de detención para forzar la finalización, pero este método no se recomienda porque detener, al igual que suspender y reanudar, son métodos caducados y no válidos.

\ 3. Utilice el método de interrupción para interrumpir el hilo.

¿Diferencia entre métodos interrumpidos e interrumpidos en Java?

interrupción: se utiliza para interrumpir subprocesos. El estado del hilo que llama a este método se establecerá en estado "interrumpido". Nota: la interrupción del hilo solo establece el bit de estado de interrupción del hilo y no lo detiene. Los usuarios necesitan monitorear

Dependiendo del estado del hilo y del procesamiento. El método que admite la interrupción del hilo (es decir, después de que se interrumpe el hilo, arrojará

método interrumpidoException) es monitorear el estado interrumpido del hilo, una vez que el estado interrumpido del hilo se establece en "estado interrumpido", se lanzará una excepción interrumpida.

interrumpido: es un método estático, verifique si la señal de interrupción actual es verdadera o falsa y borre la señal de interrupción. Si se interrumpe un hilo, la primera llamada a interrumpido devuelve verdadero y la segunda llamada y las siguientes devuelven falso.

isInterrupted: comprueba si la señal de interrupción actual es verdadera o falsa ¿Qué es un método de bloqueo?

El método de bloqueo significa que el programa esperará a que se complete el método sin hacer otras cosas. El método aceptar () de ServerSocket es esperar a que el cliente se conecte. El bloqueo aquí significa que el hilo actual se suspenderá antes de que se devuelva el resultado de la llamada y no volverá hasta que se obtenga el resultado. Además, existen métodos asincrónicos y sin bloqueo que regresan antes de que se complete la tarea.

¿Cómo se activa un hilo bloqueado en Java?

En primer lugar, los métodos wait() y notify() son para objetos. Llamar al método wait() de cualquier objeto hará que el hilo se bloquee y el bloqueo del objeto se liberará al mismo tiempo. En consecuencia, llamar el método wait() de cualquier objeto hará que el hilo se bloquee.

El método notify() desbloqueará aleatoriamente el hilo bloqueado por el objeto, pero necesita volver a adquirir el bloqueo del objeto hasta que la adquisición sea exitosa antes de continuar;

En segundo lugar, los métodos de espera y notificación deben llamarse en el bloque o método sincronizado, y el objeto de bloqueo del bloque o método sincronizado debe ser el mismo que el objeto que llama a los métodos de espera y notificación, de modo que el hilo actual haya tenido éxito antes de llamar. esperar Adquiera el bloqueo de un objeto y, después de ejecutar esperar para bloquear, el hilo actual liberará el bloqueo del objeto adquirido previamente.

¿Cuál es la diferencia entre notificar() y notificar a todos()?

Si el subproceso llama al método wait () del objeto, el subproceso estará en el grupo de espera del objeto y los subprocesos en el grupo de espera no competirán por el bloqueo del objeto.

notifyAll() activará todos los subprocesos, notify() solo activará un subproceso.

Después de llamar a notifyAll (), todos los subprocesos se moverán del grupo de espera al grupo de bloqueo y luego participarán en la competencia de bloqueo.

Si tiene éxito, continuará ejecutándose, si no, permanecerá en el grupo de bloqueos y esperará a que se libere el bloqueo antes de volver a participar en la competencia. Y notificar () solo activará un hilo, y la máquina virtual controla qué hilo activar. ¿Cómo compartir datos entre dos hilos? Compartir se logra compartiendo una variable entre dos subprocesos.

En términos generales, las variables compartidas requieren que la variable en sí sea segura para subprocesos y luego, cuando se usan en un subproceso, si hay una operación compuesta en la variable compartida, también se debe garantizar la seguridad del subproceso de la operación compuesta.

¿Cómo implementa Java la comunicación y colaboración entre múltiples subprocesos?

La comunicación y la colaboración entre subprocesos se pueden lograr interrumpiendo y compartiendo variables. Por ejemplo, el modelo clásico productor-consumidor: cuando la cola está llena, el productor debe esperar a que la cola tenga espacio antes de continuar colocando productos en ella. mientras espera Durante este período, el productor debe liberar el derecho de ocupar recursos críticos (es decir, colas). Porque si el productor no libera el derecho a ocupar recursos críticos, el consumidor no podrá consumir los bienes en la cola y la cola no tendrá espacio, por lo que el productor esperará indefinidamente. Por lo tanto, en circunstancias normales, cuando la cola esté llena, el productor renunciará al derecho de ocupar recursos críticos y entrará en el estado suspendido. Luego espere a que el consumidor consuma el producto y luego el consumidor notifica al productor que hay espacio en la cola. Asimismo, cuando la cola está vacía, el consumidor debe esperar, esperando que el productor le notifique que hay un artículo en la cola. Este proceso de comunicación entre sí es la colaboración entre hilos.

Hay dos formas comunes de comunicación y cooperación de subprocesos en Java:

1. esperar()/notificar()/notificarTodos() de la clase Objeto del hilo bloqueado sincronizado

2. Await()/signal()/signalAll() de la clase Condición del subproceso bloqueado por la clase ReentrantLock intercambia directamente datos entre subprocesos:

3. Comunicación entre subprocesos a través de canalizaciones: 1) flujo de bytes, 2) método de sincronización de flujo de caracteres y bloque de sincronización, ¿cuál es la mejor opción?

Un bloque sincronizado es una mejor opción porque no bloquea todo el objeto (por supuesto, también puedes hacer que bloquee todo el objeto). Los métodos sincronizados bloquearán todo el objeto, incluso si hay varios bloques sincronizados no relacionados en esta clase, lo que generalmente hará que dejen de ejecutarse y deban esperar para adquirir el bloqueo en este objeto.

Los bloques de sincronización deben ajustarse al principio de llamadas abiertas y solo bloquear los objetos correspondientes en los bloques de código que deben bloquearse, de modo que también se puedan evitar los puntos muertos desde el costado.

Conozca un principio: cuanto menor sea el alcance de la sincronización, mejor.

¿Qué son la sincronización de subprocesos y la exclusión mutua de subprocesos y cuáles son los métodos de implementación?

Cuando un subproceso opera con datos compartidos, se debe realizar una "operación atómica", es decir, otros subprocesos no pueden interrumpirlo antes de que se complete la operación relevante; de ​​lo contrario, la integridad de los datos se destruirá y inevitablemente se obtendrá Resultados de procesamiento incorrectos, esto es sincronización de subprocesos.

En aplicaciones de subprocesos múltiples, considere la sincronización de datos entre diferentes subprocesos y evite puntos muertos. Un punto muerto entre subprocesos se forma cuando dos o más subprocesos esperan entre sí para liberar recursos al mismo tiempo. Para evitar puntos muertos, la seguridad de los subprocesos debe lograrse mediante la sincronización.

La exclusión mutua de subprocesos se refiere a la exclusividad de los recursos del sistema de procesos compartidos cuando acceden a ellos subprocesos individuales. Cuando varios subprocesos quieren utilizar un recurso compartido, solo se permite que un subproceso lo utilice a la vez, y otros subprocesos que quieran utilizar el recurso deben esperar hasta que el ocupante del recurso lo libere. La exclusión mutua de subprocesos puede verse como un tipo especial de sincronización de subprocesos.

Los métodos de sincronización entre subprocesos se pueden dividir aproximadamente en dos categorías: modo de usuario y modo kernel. Como su nombre lo indica, el modo kernel se refiere al uso de la unicidad del objeto kernel del sistema para la sincronización. Al usarlo, es necesario cambiar entre el estado del kernel y el estado del usuario, mientras que el modo de usuario no necesita cambiar al estado del kernel y solo completa la operación en el estado del usuario.

Los métodos en modo usuario son: operaciones atómicas (por ejemplo, una única variable global), secciones críticas. Los métodos en modo kernel son: eventos, semáforos y mutex.

Cómo lograr la sincronización de subprocesos

  • Método de código sincronizado: palabra clave sincronizada método modificado bloque de código sincronizado:
  • Un bloque de código decorado con la palabra clave sincronizada.
  • Sincronización de subprocesos mediante volátil de dominio variable especial: la palabra clave volátil proporciona un mecanismo sin bloqueo para el acceso a variables de dominio
  • Utilice bloqueos reentrantes para lograr la sincronización de subprocesos: la clase reentrantlock es un bloqueo que se puede vaciar, es mutuamente excluyente e implementa la interfaz de bloqueo. Tiene el mismo comportamiento básico y semántica que el método sincronizado.

Dentro del monitor (Monitor), ¿cómo realizar la sincronización de subprocesos? ¿Qué nivel de sincronización debe tener el programa?

En la máquina virtual Java, cada objeto (objeto y clase) está asociado con un monitor a través de alguna lógica, y cada monitor está asociado con una referencia de objeto. Para realizar la función de exclusión mutua del monitor, cada objeto está asociado con un Cerrar con llave.

Una vez que el método o bloque de código se modifica mediante sincronización, esta parte se coloca en el monitor

Supervise el área para asegurarse de que solo un subproceso pueda ejecutar esta parte del código a la vez y que el subproceso no pueda ejecutar esta parte del código antes de adquirir el bloqueo.

Además, Java también proporciona dos esquemas de bloqueo: monitor explícito (bloqueo) y monitor implícito (sincronizado).

¿Qué sucede si la cola del grupo de subprocesos está llena cuando envía una tarea?

Aquí hay una distinción:

(1) Si está utilizando una cola ilimitada LinkedBlockingQueue, es decir, una cola ilimitada,

No importa, continúe agregando tareas a la cola de bloqueo para esperar la ejecución, porque LinkedBlockingQueue puede considerarse casi como una cola infinita, que puede almacenar tareas infinitamente.

(2) Si está utilizando una cola limitada como ArrayBlockingQueue, la tarea primero se agregará a

En ArrayBlockingQueue, si ArrayBlockingQueue está lleno, el

El valor de MaximumPoolSize aumenta el número de subprocesos. Si aumenta el número de subprocesos y ArrayBlockingQueue continúa lleno, se utilizará la estrategia de rechazo.

RejectedExecutionHandler maneja tareas completas, el valor predeterminado es AbortPolicy ¿Qué es la seguridad de subprocesos? ¿Los servlets son seguros para subprocesos?

La seguridad de subprocesos es un término en programación, lo que significa que cuando se llama a un método en un entorno de subprocesos múltiples, puede manejar correctamente las variables compartidas entre múltiples subprocesos, de modo que la función del programa se pueda completar correctamente.

Los servlets no son seguros para subprocesos. Los servlets son subprocesos múltiples de instancia única. Cuando varios subprocesos acceden al mismo método al mismo tiempo, no se puede garantizar la seguridad de los subprocesos de las variables compartidas.

La acción de Struts2 es de instancias múltiples y subprocesos múltiples, y es segura para subprocesos. Cada vez que llega una solicitud, se asignará una nueva acción a esta solicitud y se destruirá una vez completada la solicitud.

¿El controlador de SpringMVC es seguro para subprocesos? No, el flujo de procesamiento es similar al de Servlet.

La ventaja de Struts2 es que no necesita considerar problemas de seguridad de subprocesos; Servlet y SpringMVC deben considerar subprocesos

Problemas de seguridad, pero el rendimiento se puede mejorar sin tener que lidiar con demasiado gc. Puede usar ThreadLocal para lidiar con problemas de subprocesos múltiples.

¿Cómo garantizar la seguridad de la operación multiproceso en programas Java?

Método 1: utilice clases de seguridad, como las clases de java.util.concurrent, y utilice la clase atómica AtomicInteger

Método 2: utilizar bloqueo automático sincronizado.

Método 3: utilizar el bloqueo manual.

El código de muestra de Java para el bloqueo manual es el siguiente:

1 Lock lock = new ReentrantLock(); 
2 lock. lock(); 
3 try {
4	System. out. println("获得锁");
5	} catch (Exception e) {
6	// TODO: handle exception
7	} finally {
8	System. out. println("释放锁");
9	lock. unlock();
10	}

¿Cuál es su comprensión de la prioridad del hilo?

Cada subproceso tiene una prioridad. En términos generales, los subprocesos de alta prioridad tendrán prioridad en tiempo de ejecución, pero esto depende de la implementación de la programación de subprocesos, que está relacionada con el sistema operativo (dependiente del sistema operativo). Podemos definir la prioridad de los subprocesos, pero esto no garantiza que los subprocesos de alta prioridad se ejecuten antes que los de baja prioridad. La prioridad del hilo es una variable int (de 1 a 10), 1 para baja prioridad, 10 para alta prioridad. La programación de prioridad de subprocesos de Java se confiará al sistema operativo para que la maneje, por lo que está relacionada con la prioridad específica del sistema operativo. Si no hay una necesidad especial, generalmente no es necesario establecer la prioridad de subproceso.

La programación de prioridad de subprocesos de Java se confiará al sistema operativo para que la maneje, por lo que está relacionada con la prioridad específica del sistema operativo. Si no hay una necesidad especial, generalmente no es necesario establecer la prioridad de subproceso.

El método de construcción de la clase de hilo y por qué hilo se llama al bloque estático

Ésta es una pregunta muy complicada y astuta. Recuerde: el constructor de la clase de hilo, el bloque estático es el

New es llamado por el hilo donde se encuentra la clase de hilo, y el código en el método de ejecución es llamado por el hilo mismo.

Si la afirmación anterior te confunde, déjame darte un ejemplo, asumiendo que Thread2 es nuevo.

Thread1, Thread2 es nuevo en la función principal, entonces:

(1) El método de construcción y el bloque estático de Thread2 son llamados por el hilo principal, y el método run () de Thread2 es llamado por el propio Thread2.

(2) Thread2 llama al método de construcción y al bloque estático de Thread1, y el propio Thread1 llama al método run () de Thread1.

¿Cómo obtener un archivo de volcado de subprocesos en Java? ¿Cómo se obtiene la pila de subprocesos en Java?

Un archivo de volcado es una imagen de memoria de un proceso. El estado de ejecución del programa se puede guardar en el archivo de volcado a través del depurador.

En Linux, puede obtener el archivo de volcado de la aplicación Java mediante el comando kill -3 PID (el ID del proceso Java).

En Windows, puedes presionar Ctrl + Break para obtenerlo. De esta manera, la JVM imprimirá el archivo de volcado del hilo en el archivo de salida estándar o de error, que puede imprimirse en la consola o en el archivo de registro, y la ubicación específica depende de la configuración de la aplicación.

¿Qué sucede cuando ocurre una excepción mientras se ejecuta un hilo? Si no se detecta la excepción, el hilo dejará de ejecutarse.

Thread.UncaughtExceptionHandler es una interfaz integrada que se utiliza para manejar la interrupción repentina de subprocesos causada por excepciones no detectadas. Cuando una excepción no detectada causará una interrupción del hilo, la JVM utilizará

Thread.getUncaughtExceptionHandler() para consultar el hilo

UncaughtExceptionHandler pasa el hilo y la excepción como parámetros al método uncaughtException() del controlador para su procesamiento.

¿Qué excepciones serán causadas por demasiados subprocesos de Java?

  • La sobrecarga de vida útil de los subprocesos es muy alta y consume demasiado.

  • Recursos de CPU Si el número de subprocesos ejecutables es mayor que el número de procesadores disponibles, algunos subprocesos estarán inactivos. Una gran cantidad de subprocesos inactivos ocupará mucha memoria y ejercerá presión sobre el recolector de basura, y una gran cantidad de subprocesos también generará otros gastos generales de rendimiento al competir por los recursos de la CPU.

  • Estabilidad reducida JVM tiene un límite en la cantidad de subprocesos que se pueden crear. Este valor límite variará según la plataforma y está sujeto a múltiples factores, incluidos los parámetros de inicio de JVM, el tamaño de la pila de solicitudes en el constructor de subprocesos y las limitaciones. del sistema operativo subyacente en subprocesos, etc. Si se violan estas restricciones, se puede generar una excepción OutOfMemoryError.

teoría de la concurrencia

modelo de memoria java

¿Cuál es el propósito de la recolección de basura en Java? ¿Cuándo ocurre la recolección de basura? La recolección de basura ocurre cuando hay objetos sin referencia en la memoria u objetos que están fuera de alcance. El propósito de la recolección de basura es identificar y descartar objetos que la aplicación ya no utiliza para liberar y reutilizar recursos.

Si la referencia del objeto se establece en nula, ¿el recolector de basura liberará inmediatamente la memoria ocupada por el objeto?

No, el objeto será reciclable en el próximo ciclo de devolución de llamada basura.

Es decir, no será reclamado inmediatamente por el recolector de basura, pero la memoria que ocupa será liberada en la siguiente recolección de basura.

¿Cuándo se llama al método finalize()? ¿Cuál es el propósito de un destructor (finalización)?

1) Cuando el recolector de basura decide reciclar un objeto, ejecutará el método finalize () del objeto;

finalize es un método de la clase Object, la declaración de este método en la clase Object protected void finalize() throwable { }

Cuando se ejecuta el recolector de basura, se llamará al método finalize() del objeto recuperado, y este método se puede anular para realizar la recuperación de sus recursos. Nota: Una vez que el recolector de basura esté listo para liberar la memoria ocupada por el objeto, primero llamará al método finalize() del objeto y el espacio de memoria ocupado por el objeto no se recuperará hasta que se produzca la siguiente acción de recolección de basura.

2) GC es originalmente para el reciclaje de memoria, ¿qué más debe hacer la aplicación al finalizar? La respuesta es que la mayoría de las veces no es necesario hacer nada (es decir, no es necesario sobrecargarse). Solo en algunos casos muy especiales, por ejemplo, si llama a algunos métodos nativos (generalmente escritos en C), puede llamar a la función de liberación de C en la finalización.

Reordenar las dependencias de datos ¿Por qué se reordena el código?

[Falló la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-leeching, se recomienda guardar la imagen y cargarla directamente (img-fsRmIju2-1692509038139) (04-Preguntas de la entrevista de programación concurrente (última versión de 2020) -focus .assets/clip_image001.gif )] Al ejecutar un programa, para mejorar el rendimiento, los procesadores y compiladores a menudo reordenan las instrucciones, pero no se pueden reordenar a voluntad. No es así como se desea ordenar. Debe cumplir con los dos siguientes condiciones: en un solo hilo El resultado de la ejecución del programa no se puede cambiar en el entorno, no se permite reordenar si hay dependencia de datos. Cabe señalar que el reordenamiento no afectará el resultado de la ejecución del entorno de un solo subproceso, pero sí Destruye la semántica de ejecución de subprocesos múltiples.

La diferencia entre la regla como si fuera serial y la regla de sucede antes

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-QlxYssLh-1692509038140) (04-Preguntas de la entrevista de programación concurrente (última versión de 2020) -focus .assets/clip_image002.gif )] La semántica como si fuera serie garantiza que el resultado de la ejecución del programa en un solo subproceso no cambie, y la relación sucede antes garantiza que el resultado de la ejecución de un programa multiproceso sincronizado correctamente no cambie. . [Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo de enlace antirrobo, se recomienda guardar la imagen y cargarla directamente (img-MUYsFGNW-1692509038140) (04-Preguntas de la entrevista de programación concurrente (última versión de 2020) - focus.assets/clip_image003.gif )] La semántica como si fuera serie crea una ilusión para los programadores que escriben programas de un solo subproceso: los programas de un solo subproceso se ejecutan en el orden del programa. La relación sucede antes crea una ilusión para los programadores que escriben programas multiproceso sincronizados correctamente: los programas multiproceso correctamente sincronizados se ejecutan en el orden especificado por sucede antes.

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo de enlace antirrobo, se recomienda guardar la imagen y cargarla directamente (img-k6LqCuxd-1692509038140) (04-Preguntas de la entrevista de programación concurrente (última versión de 2020) - focus.assets/clip_image004.gif )] El propósito de la semántica como si fuera en serie y sucede antes es mejorar el paralelismo de la ejecución del programa tanto como sea posible sin cambiar los resultados de la ejecución del programa.

palabra clave concurrente

sincronizado

¿Qué hace sincronizado?

En Java, la palabra clave sincronizada se utiliza para controlar la sincronización de subprocesos, es decir, en un entorno de subprocesos múltiples, para controlar que el segmento de código sincronizado no sea ejecutado por varios subprocesos al mismo tiempo. sincronizado puede modificar clases, métodos y variables.

Además, en las primeras versiones de Java, sincronizado es un bloqueo pesado, que es ineficiente porque el monitoreo

El bloqueo del monitor se implementa confiando en el bloqueo Mutex del sistema operativo subyacente, Java.

Los subprocesos se asignan a subprocesos nativos del sistema operativo. Si desea suspender o reactivar un subproceso, necesita la ayuda del sistema operativo para completarlo, y el sistema operativo debe cambiar del modo de usuario al modo kernel al cambiar entre subprocesos. La transición entre estos estados lleva un tiempo relativamente largo. tiempo y el costo de tiempo Relativamente alto, razón por la cual la sincronización temprana era ineficiente. Afortunadamente, después de Java 6, el funcionario de Java

Desde el nivel de JVM, la sincronización está muy optimizada, por lo que la eficiencia del bloqueo sincronizado actual también está muy optimizada. JDK1.6 introduce una gran cantidad de optimizaciones en la implementación de bloqueos, como bloqueos de giro, bloqueos de giro adaptativos, eliminación de bloqueos, engrosamiento de bloqueos, bloqueos sesgados, bloqueos livianos y otras tecnologías para reducir la sobrecarga de las operaciones de bloqueo.

Cuéntame cómo usas la palabra clave sincronizada, ¿se ha usado en el proyecto?

Hay tres formas principales de utilizar la palabra clave sincronizada:

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-0qHcBdL6-1692509038141) (04-Preguntas de la entrevista de programación concurrente (última versión de 2020) -clave points.assets/clip_image005.gif)] Método de instancia modificado: actúa sobre la instancia del objeto actual para bloquear y obtiene el bloqueo de la instancia del objeto actual antes de ingresar el código de sincronización.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TQ3tUhed-1692509038141)(04-并发编程面试题(2020最新版)-重点.assets/clip_image006.gif)] 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H1QgANbK-1692509038141)(04-并发编程面试题(2020最新版)-重点.assets/clip_image007.gif)] 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定

对象的锁。

总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实

例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具

有缓存功能!下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” 双重校验锁实现对象单例(线程安全)

1 public class Singleton {
2
3 private volatile static Singleton uniqueInstance;
4
5	private Singleton() {
6	}
7
8	public static Singleton getUniqueInstance() {
9	//先判断对象是否已经实例过,没有实例化过才进入加锁代码
10	if (uniqueInstance == null) {
11	//类对象加锁
12	synchronized (Singleton.class) {
13	if (uniqueInstance == null) {
14	uniqueInstance = new Singleton();
15	}
16	}
17	}
18	return uniqueInstance;
19	}
20	}

Además, es necesario tener en cuenta que UniqueInstance se modifica con la palabra clave volátil. También es necesario modificar UniqueInstance con la palabra clave volátil, UniqueInstance.

= new Singleton(); Este código en realidad se ejecuta en tres pasos:

  1. Asignar espacio de memoria para instancia única

  2. Inicializar instancia única

  3. Apunte una instancia única a la dirección de memoria asignada

Pero debido a que JVM tiene la característica de reorganización de instrucciones, la secuencia de ejecución puede convertirse en 1->3->2. La reorganización de instrucciones no causará problemas en un entorno de un solo subproceso, pero en un entorno de múltiples subprocesos hará que un subproceso obtenga una instancia que aún no se ha inicializado. Por ejemplo, el hilo T1 ejecuta 1 y 3, en este momento T2 llama

Después de que getUniqueInstance() descubre que UniqueInstance no está vacío, devuelve UniqueInstance, pero UniqueInstance aún no se ha inicializado. El uso de volátil puede prohibir la reorganización de instrucciones de la JVM para garantizar que también pueda ejecutarse normalmente en un entorno de subprocesos múltiples. ¿Cuénteme sobre el principio de implementación subyacente de sincronizado?

Sincronizado es una palabra clave en Java y el proceso de bloqueo y desbloqueo mostrado no se ve durante el uso. Por lo tanto, es necesario ver el archivo de código de bytes correspondiente mediante el comando javap. El caso de los bloques de declaraciones síncronos sincronizados.

1	public class SynchronizedDemo {
2	public void method() {
3	synchronized (this) {
4	System.out.println("synchronized 代码块");
5	}
6	}
7	}

A través de la instrucción de desmontaje JDK javap -c -v SynchronizedDemo

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-3R8ToFF1-1692509038141) (04-Preguntas de la entrevista de programación concurrente (última versión de 2020) -focus .assets/image-20201109183510197 .png)]

Se puede ver que hay una palabra de monitor antes y después de la ejecución del bloque de código síncrono, de la cual el frente es

monitorenter, este último es salir de monitorexit. No es difícil imaginar que un hilo también ejecuta un bloque de código sincrónico, primero para adquirir un bloqueo, y el proceso de adquirir un bloqueo es monitorenter, después de ejecutar el bloque de código, para liberar el bloquear, liberar el bloqueo es ejecutar la instrucción monitorexit.

¿Por qué hay dos salidas de monitor?

Esto es principalmente para evitar que el hilo salga debido a una excepción en el bloque de código de sincronización, pero el bloqueo no se libera, lo que inevitablemente provocará un punto muerto (el hilo en espera nunca adquirirá el bloqueo). Por lo tanto, la última salida del monitor es para garantizar que el bloqueo también se pueda liberar en condiciones anormales para evitar un punto muerto.

Solo existe un indicador como ACC_SYNCHRONIZED, que indica que cuando un hilo ingresa a este método, se requiere monitorenter y cuando sale de este método, se requiere monitorexit.

El principio de reentrante sincronizado.

Un bloqueo reentrante significa que después de que un hilo adquiere el bloqueo, el hilo puede continuar adquiriendo el bloqueo. El principio subyacente mantiene un contador. Cuando un hilo adquiere el bloqueo, el contador se incrementa en uno. Cuando se vuelve a adquirir el bloqueo, el contador se incrementa en uno. Cuando se libera el bloqueo, el contador se reduce en uno. Cuando el hilo adquiere el bloqueo, el contador se incrementa en uno. El valor del contador es 0, indica que el bloqueo no está retenido por ningún hilo. Sí, otros hilos pueden competir para adquirir el bloqueo.

¿Qué es el giro?

Muchos códigos sincronizados son solo algunos códigos muy simples y el tiempo de ejecución es muy rápido. En este momento, bloquear todos los subprocesos en espera puede ser una operación indigna, porque el bloqueo de subprocesos implica el problema de cambiar entre el modo de usuario y el modo kernel. Dado que el código en sincronizado se ejecuta muy rápido, es aconsejable no bloquear el hilo que espera el bloqueo, sino realizar un bucle ocupado en el límite de sincronizado, que es el giro. Si ha realizado varios bucles y descubrió que no ha adquirido el bloqueo, entonces bloquee, esta puede ser una mejor estrategia.

¿Cuál es el principio de actualización de bloqueo sincronizado en subprocesos múltiples?

Principio de actualización de bloqueo sincronizado: hay un campo de ID de subproceso en el encabezado del objeto de bloqueo. Cuando se accede al ID de subproceso por primera vez, el ID de subproceso está vacío y JVM le permite mantener un bloqueo sesgado y establece el ID de subproceso como su ID de hilo. Primero juzgue si threadid es consistente con su ID de hilo, si

Si es consistente, el objeto se puede usar directamente. Si no es consistente, el bloqueo sesgado se actualizará a un bloqueo liviano y el bloqueo se adquirirá a través de una cierta cantidad de ciclos de giro. Después de una cierta cantidad de ejecuciones, si el objeto a utilizar no se ha obtenido normalmente, entonces El candado se actualizará de un candado liviano a un candado pesado, y este proceso constituye una actualización de un candado sincronizado.

El propósito de la actualización del bloqueo: la actualización del bloqueo es reducir el consumo de rendimiento causado por el bloqueo. Después de Java 6, se optimiza la implementación de sincronizado y el bloqueo sesgado se actualiza a un bloqueo liviano y luego a un bloqueo pesado, lo que reduce el consumo de rendimiento causado por el bloqueo.

¿Cómo sabe el hilo B que el hilo A ha modificado la variable?

(1) variable modificadora volátil

(2) Método sincronizado para modificar y modificar variables.

(3) esperar/notificar

(4) durante la encuesta

Después de que un hilo ingresa al método sincronizado A de un objeto, ¿pueden otros subprocesos ingresar al método sincronizado B de este objeto?

no puedo. Otros subprocesos solo pueden acceder a los métodos no sincronizados del objeto y los métodos sincronizados no pueden ingresar. Debido a que el modificador sincronizado en el método no estático requiere que el bloqueo del objeto se adquiera cuando se ejecuta el método, si el bloqueo del objeto ya se ha ingresado en el método A, entonces el hilo que intenta ingresar al método B solo puede esperar. para el grupo de esclusas

(Tenga en cuenta que no está esperando el grupo) para esperar el bloqueo del objeto.

sincronizado, volátil, comparación CAS

(1) sincronizado es un bloqueo pesimista, que es preventivo y provocará el bloqueo de otros subprocesos. (2) volátil proporciona visibilidad de variables compartidas de subprocesos múltiples y prohíbe la optimización del reordenamiento de instrucciones.

(3) CAS es un bloqueo optimista basado en la detección de conflictos (sin bloqueo)

¿Cuál es la diferencia entre sincronizado y Lock?

  • En primer lugar, sincronizado es una palabra clave incorporada en Java. A nivel de JVM, Lock es una clase de Java;
  • sincronizado puede bloquear clases, métodos y bloques de código, mientras que lock solo puede bloquear bloques de código.
  • Sincronizado no necesita adquirir y liberar bloqueos manualmente. Es fácil de usar. Cuando ocurre una excepción, liberará automáticamente el bloqueo y no causará un punto muerto; mientras que el bloqueo debe bloquearse y liberarse por sí solo. Si no se usa correctamente y no hay unLock() para liberar el bloqueo, provocará un bloqueo muerto.
  • A través de Lock, puede saber si el bloqueo se adquirió correctamente, pero la sincronización no.

¿Cuál es la diferencia entre sincronizado y ReentrantLock?

Synchronized es la misma palabra clave que if, else, for y while, y ReentrantLock es una clase, que es la diferencia esencial entre los dos. Dado que ReentrantLock es una clase, proporciona más de

sincronizado tiene características cada vez más flexibles, se puede heredar, puede tener métodos y puede tener varias variables de clase

La implementación temprana de sincronizado es relativamente ineficiente y, en comparación con ReentrantLock, el rendimiento de la mayoría de los escenarios es menor.

La diferencia es bastante grande, pero la sincronización se ha mejorado mucho en Java 6.

El mismo punto: ambas son cerraduras reentrantes y ambas son cerraduras reentrantes. El concepto de "cerradura reentrante" es: usted puede volver a adquirir su propia cerradura interna. Por ejemplo, un hilo adquiere el bloqueo de un objeto y el bloqueo del objeto no se ha liberado en este momento. Cuando quiere adquirir el bloqueo del objeto nuevamente, aún puede adquirirlo. Si el bloqueo no es reentrante, causará un punto muerto.

Cada vez que el mismo hilo adquiere un bloqueo, el contador de bloqueo se incrementa en 1, por lo que el bloqueo no se puede liberar hasta que el contador de bloqueo caiga a 0. Las principales diferencias son las siguientes:

  • ReentrantLock es más flexible de usar, pero debe haber una acción cooperativa para liberar el bloqueo;
  • ReentrantLock debe adquirir y liberar el bloqueo manualmente, mientras que sincronizado no necesita liberar y abrir el bloqueo manualmente;
  • ReentrantLock solo es adecuado para bloqueos de bloques de código, mientras que sincronizado puede modificar clases, métodos, variables, etc.
  • El mecanismo de bloqueo de los dos es en realidad diferente. La capa inferior de ReentrantLock llama al método de estacionamiento de Unsafe para bloquear, y la operación sincronizada debe ser la palabra de marca en el encabezado del objeto.
  • Cada objeto en Java se puede utilizar como bloqueo, que es la base de la sincronización sincronizada: método de sincronización común, el bloqueo es el objeto de instancia actual
  • Método de sincronización estática, el bloqueo es el objeto de clase de la clase actual
  • Bloque de método sincronizado, el bloqueo es el objeto entre paréntesis

volátil

El papel de la palabra clave volátil

Para mayor visibilidad, Java proporciona la palabra clave volátil para garantizar la visibilidad y prohibir la reorganización de las instrucciones.

volátil proporciona garantías de que sucede antes, lo que garantiza que las modificaciones realizadas por un subproceso sean visibles para otros subprocesos. Cuando una variable compartida es modificada por volátil, garantizará que el valor modificado se actualice inmediatamente en la memoria principal, y cuando otros subprocesos necesiten leerlo, irá a la memoria para leer el nuevo valor.

Desde un punto de vista práctico, una función importante de volátil es combinarse con CAS para garantizar la atomicidad. Para obtener más detalles, consulte las clases del paquete java.util.concurrent.atomic, como

Entero atómico。

Volátil se utiliza a menudo para una sola operación (lectura única o escritura única) en un entorno de subprocesos múltiples.

¿Se pueden crear matrices volátiles en Java?

Sí, se pueden crear matrices volátiles en Java, pero solo una referencia a la matriz, no la matriz completa. Significa que si cambia la matriz a la que apunta la referencia, estará protegida por volátil, pero si varios subprocesos cambian los elementos de la matriz al mismo tiempo, el identificador volátil no podrá reproducir la protección anterior.

¿Cuál es la diferencia entre una variable volátil y una variable atómica?

Una variable volátil puede garantizar una relación de precedencia, es decir, una operación de escritura ocurrirá antes de una operación de lectura posterior, pero no garantiza la atomicidad. Por ejemplo, si la variable de conteo se modifica con volátil, entonces la operación de conteo ++ no es atómica.

El método atómico proporcionado por la clase AtomicInteger puede hacer que esta operación sea atómica, como

El método getAndIncrement() incrementará atómicamente el valor actual en uno, y otros tipos de datos y variables de referencia también pueden realizar operaciones similares.

¿Puede volátil hacer atómica una operación no atómica?

La función principal de la palabra clave volátil es hacer que las variables sean visibles en varios subprocesos, pero no se puede garantizar la atomicidad. Para que varios subprocesos accedan a la misma variable de instancia, se requieren bloqueos para la sincronización.

Aunque volatile solo puede garantizar visibilidad pero no atomicidad, modificar long y double con volatile puede garantizar la atomicidad de sus operaciones.

Como puede ver en la especificación Oracle Java:

  • Para 64 bits de largo y doble, si no se modifican con volátiles, es posible que sus operaciones no sean atómicas. Cuando está en funcionamiento, se puede dividir en dos pasos y cada vez funciona con 32 bits.
  • Si usa volátil para modificar long y double, entonces su lectura y escritura son operaciones atómicas, y la lectura y escritura de direcciones de referencia de 64 bits son operaciones atómicas.
  • Al implementar la JVM, puede elegir libremente si leer y escribir operaciones largas y duplicadas como operaciones atómicas.
  • Se recomienda que la JVM implemente operaciones atómicas.

¿Cuál es la práctica del modificador volátil?

patrón singleton

Si inicialización diferida: Sí Si seguridad de subprocesos múltiples: Sí

Dificultad de implementación: más compleja Descripción: Para posibles problemas como Double-Check (por supuesto, la probabilidad de que esto ocurra es muy pequeña, pero todavía hay algunos ~), la solución es: solo es necesario agregar volátil a la declaración de instancia

Una de las funciones de la palabra clave volátil es prohibir la reorganización de instrucciones. Después de que la instancia se declara como volátil, habrá una barrera de memoria para su operación de escritura (¿qué es una barrera de memoria?), De modo que antes de que se complete su asignación , no es necesario llamar a la operación de lectura. Nota: Lo que evita volatile no es la reorganización de la instrucción de [1-2-3] dentro de la oración singleton = newSingleton(), sino que garantiza que en una operación de escritura ([1-

1 public class Singleton7 {
2 
3  private static volatile Singleton7 instance = null;
4 
5  private Singleton7() {}
6
7  public static Singleton7 getInstance() {
8  if (instance == null) {
9  synchronized (Singleton7.class) {
10  if (instance == null) {
11  instance = new Singleton7(); 
12  } 
13  } 
14  }
15 
16  return instance;
17  }
18 
19 }

¿Cuál es la diferencia entre sincronizado y volátil?

Sincronizado significa que solo un subproceso puede adquirir el bloqueo del objeto, ejecutar código y bloquear otros subprocesos.

volátil significa que la variable es indeterminada en los registros de la CPU y debe leerse desde la memoria principal. Garantizar la visibilidad de las variables en un entorno multiproceso; prohibir el reordenamiento de instrucciones.

la diferencia

  • volátil es un modificador de variable; sincronizado puede modificar clases, métodos y variables.
  • Volátil solo puede realizar la visibilidad de modificación de las variables, pero no puede garantizar la atomicidad, mientras que sincronizado puede garantizar la visibilidad de modificación y la atomicidad de las variables.
  • Volátil no provocará el bloqueo de subprocesos; sincronizado puede provocar el bloqueo de subprocesos.
  • El compilador no optimizará las variables marcadas con volátil; el compilador puede optimizar las variables marcadas con sincronizado.
  • La palabra clave volátil es una implementación ligera de sincronización de subprocesos, por lo que el rendimiento volátil es definitivamente mejor que la palabra clave sincronizada. Pero la palabra clave volátil sólo se puede utilizar para variables y no

La palabra clave sincronizada puede modificar métodos y bloques de código. La palabra clave sincronizada está en

Después de JavaSE1.6, la eficiencia de ejecución ha mejorado significativamente después de la introducción de bloqueos sesgados y bloqueos livianos y varias otras optimizaciones para reducir el consumo de rendimiento causado por la adquisición y liberación de bloqueos. La clave para usar sincronizado en el desarrollo real Todavía hay más escenas con palabras.

final

¿Qué es un objeto inmutable y cómo ayuda a escribir aplicaciones concurrentes?

Objetos inmutables (Objetos inmutables) significa que una vez que se crea el objeto, su estado (los datos del objeto, es decir, el valor del atributo del objeto) no se puede cambiar; de lo contrario, es un objeto mutable (Objetos mutables).

La clase de un objeto inmutable se llama clase inmutable (Clase Inmutable). La biblioteca de clases de la plataforma Java contiene muchos tipos diferentes.

Clases mutables como String, clases contenedoras para tipos primitivos, BigInteger, BigDecimal, etc.

Un objeto es inmutable solo si: su estado no se puede modificar después de la creación; todos los campos son finales; y se creó correctamente (no se produjo ningún escape de esta referencia durante la creación).

El objeto inmutable garantiza la visibilidad de la memoria del objeto y la lectura del objeto inmutable no requiere medios de sincronización adicionales, lo que mejora la eficiencia de ejecución del código.

Sistema de bloqueo

Introducción a Lock y AQS

¿Qué es la interfaz Lock en la API de concurrencia de Java? ¿Cuáles son sus ventajas sobre el síncrono?

La interfaz Lock proporciona operaciones de bloqueo más escalables que los métodos sincronizados y los bloques sincronizados. Permiten una estructura más flexible, pueden tener propiedades completamente diferentes y pueden admitir múltiples objetos de condición de clases relacionadas.

Sus ventajas son:

(1) Puede hacer que la cerradura sea más justa

(2) Permite que los subprocesos respondan a interrupciones mientras esperan bloqueos

(3) El hilo puede intentar adquirir el candado y regresar inmediatamente o esperar un período de tiempo en el que no se pueda adquirir el candado.

(4) Los candados se pueden adquirir y liberar en diferentes órdenes en diferentes ámbitos.

En general, Lock es una versión extendida de sincronizado. Lock proporciona operaciones de bloqueo incondicionales, sondeables (método tryLock), temporizados (método tryLock con parámetros), interrumpibles (lockInterruptfully) y de cola de múltiples condiciones (método newCondition). Además, las clases de implementación de Lock básicamente admiten bloqueos injustos (predeterminados) y bloqueos justos, y sincronizados solo admiten bloqueos injustos. Por supuesto, en la mayoría de los casos, los bloqueos injustos son una opción eficiente.

La comprensión del bloqueo optimista y el bloqueo pesimista y cómo implementarlos, ¿cuáles son los métodos de implementación?

Bloqueo pesimista: siempre asuma una mala situación, cada vez que va a obtener los datos, piensa que otros los modificarán, por lo que cada vez que obtenga los datos, los bloqueará, de modo que otros se bloquearán hasta obtener el bloqueo si Quieren obtener los datos. Muchos de estos mecanismos de bloqueo se utilizan en bases de datos relacionales tradicionales, como bloqueos de fila, bloqueos de tabla, etc., bloqueos de lectura, bloqueos de escritura, etc., todos los cuales se bloquean antes de realizar las operaciones. Otro ejemplo es la implementación de la palabra clave sincronizada en Java, que también es un bloqueo pesimista.

Bloqueo optimista: como sugiere el nombre, es muy optimista. Cada vez que va a obtener los datos, piensa que otros no los modificarán, por lo que no se bloquearán. Pero al actualizar, juzgará si otros han actualizado los datos. datos durante este período. Puede utilizar mecanismos como números de versión. El bloqueo optimista es adecuado para tipos de aplicaciones de lectura múltiple, lo que puede mejorar el rendimiento. Al igual que el mecanismo de condición de escritura proporcionado por la base de datos, en realidad es un bloqueo optimista proporcionado. existir

Las clases de variables atómicas del paquete java.util.concurrent.atomic en Java se implementan mediante CAS, un método de implementación de bloqueo optimista.

Implementación de bloqueo optimista:

1. Utilice el identificador de versión para determinar si los datos leídos son consistentes con los datos enviados. Luego del envío, modifique la identificación de la versión, si es inconsistente puede adoptar la estrategia de descartar e intentar nuevamente.

2. Comparar e intercambiar en Java es CAS. Cuando varios subprocesos intentan usar CAS para actualizar la misma variable al mismo tiempo, solo uno de los subprocesos puede actualizar el valor de la variable, mientras que otros subprocesos fallan y el subproceso fallido No fue suspendido, pero le dijeron que perdiera esta competición y que lo intentara de nuevo. Hay tres operandos en la operación CAS: la ubicación de memoria (V) que debe leerse y escribirse y el valor original esperado para comparar.

(A) y el nuevo valor a escribir (B). Si el valor en la ubicación de memoria V coincide con el valor antiguo esperado A, el procesador actualiza automáticamente el valor de la ubicación con el nuevo valor B. De lo contrario el procesador no hace nada.

¿Qué es CAS?

CAS es la abreviatura de comparar e intercambiar, que es lo que llamamos comparación e intercambio.

cas es una operación basada en bloqueos y es un bloqueo optimista. Los bloqueos en Java se dividen en bloqueos optimistas y bloqueos pesimistas.

El bloqueo pesimista consiste en bloquear el recurso, y el siguiente subproceso solo puede acceder a él después de que un subproceso que ha adquirido el bloqueo previamente lo libere. El bloqueo optimista adopta una actitud amplia y procesa recursos sin bloquear de cierta manera, como agregar versiones a los registros para obtener datos, lo que mejora enormemente el rendimiento en comparación con el bloqueo pesimista.

Una operación CAS tiene tres operandos: una ubicación de memoria (V), un valor antiguo esperado (A) y un valor nuevo (B). Si el valor en la dirección de memoria es el mismo que el valor en A, actualice el valor en la memoria a B.

CAS obtiene datos a través de un bucle infinito, si está en la primera ronda del bucle, un hilo obtiene la dirección interna

El valor de es modificado por el hilo b, luego el hilo a necesita girar y no será posible ejecutarlo hasta el siguiente ciclo.

La mayoría de las clases del paquete java.util.concurrent.atomic se implementan mediante operaciones CAS.

(AtomicInteger,AtomicBoolean,AtomicLong)。

¿Qué pasa con el CAS?

1. Problema de ABA: por ejemplo, un subproceso uno recupera A de la ubicación de memoria V, en este momento otro subproceso dos también recupera A de la memoria, y dos realizan algunas operaciones para cambiarlo a B, y luego dos devuelven los datos en la ubicación V se convierte en A. En este momento, el hilo uno realiza la operación CAS y descubre que A todavía está en la memoria, y luego una operación tiene éxito. Aunque la operación CAS para el subproceso uno fue exitosa, puede haber un problema subyacente. A partir de Java1.5, el paquete atómico de JDK proporciona una clase AtomicStampedReference para resolver el problema ABA.

2. El tiempo del ciclo es largo y el costo alto:

En el caso de una competencia severa de recursos (conflicto grave de subprocesos), la probabilidad de que CAS gire será relativamente alta, desperdiciando más recursos de CPU y la eficiencia es menor que la de sincronizado.

3. Solo se puede garantizar la operación atómica de una variable compartida: cuando realizamos una operación en una variable compartida, podemos usar el método CAS cíclico para garantizar la operación atómica, pero cuando operamos en múltiples variables compartidas, el CAS cíclico no puede garantizar la Operación atómica Sexo, puedes usar candados en este momento. ¿Qué es el punto muerto?

Cuando el subproceso A mantiene el bloqueo exclusivo a e intenta adquirir el bloqueo exclusivo b, el subproceso B mantiene el bloqueo exclusivo

b, e intenta adquirir el bloqueo exclusivo a, habrá un fenómeno de bloqueo entre los dos subprocesos de AB porque mantienen los bloqueos que necesitan entre sí, lo que llamamos punto muerto.

¿Cuáles son las condiciones para un punto muerto? ¿Cómo evitar un punto muerto?

Condiciones necesarias para que se produzca un punto muerto:

1. Condiciones de exclusión mutua: La llamada exclusión mutua significa que un proceso monopoliza recursos dentro de un período de tiempo determinado.

2. Condiciones de solicitud y retención: cuando un proceso se bloquea debido a la solicitud de recursos, no soltará los recursos obtenidos.

3. Condición de no privación: el proceso ha obtenido recursos y no se puede privar por la fuerza antes de que se agoten.

4. Condición de espera circular: se forma una relación de recursos de espera cíclica de cabeza a cola entre varios procesos.

Estas cuatro condiciones son las condiciones necesarias para el punto muerto: mientras el sistema esté en punto muerto, estas condiciones deben ser verdaderas, y mientras no se cumpla una de las condiciones anteriores, no se producirá el punto muerto.

Comprender las causas de los puntos muertos, especialmente las cuatro condiciones necesarias para los puntos muertos, puede evitarlos, prevenirlos y resolverlos tanto como sea posible.

Se pueden utilizar los siguientes métodos para evitar el punto muerto:

  • Intente utilizar el método tryLock (tiempo de espera prolongado, unidad TimeUnit) (ReentrantLock,
  • ReentrantReadWriteLock), establece el período de tiempo de espera, el tiempo de espera puede salir para evitar un punto muerto.
  • Intente utilizar la clase concurrente java.util.concurrent en lugar de su propio candado escrito a mano. Intente reducir la granularidad del uso del bloqueo y trate de no utilizar el mismo bloqueo para varias funciones.
  • Minimiza los bloques de código sincronizados.

¿La diferencia entre estancamiento y estancamiento, la diferencia entre estancamiento y hambre?

Punto muerto: Se refiere a un fenómeno en el que dos o más procesos (o subprocesos) se esperan entre sí debido a la competencia por los recursos durante el proceso de ejecución, si no hay una fuerza externa no podrán avanzar.

Livelock: la tarea o el ejecutor no se bloquea porque no se cumplen algunas condiciones, lo que resulta en intentos repetidos, fallas, intentos, fallas.

La diferencia entre un livelock y un deadlock es que la entidad en el livelock cambia constantemente su estado, lo que se llama "vivo", mientras la entidad en el interbloqueo está esperando; el livelock puede resolverse por sí solo y el interbloqueo no .

Inanición: uno o más subprocesos no pueden obtener los recursos necesarios por diversas razones, lo que resulta en un estado que no se ha podido ejecutar.

Causas del hambre en Java:

1. El subproceso de alta prioridad consume todo el tiempo de CPU del subproceso de baja prioridad.

2. El subproceso está bloqueado permanentemente en un estado esperando ingresar al bloque sincronizado, porque otros subprocesos siempre pueden continuar accediendo al bloque sincronizado antes que él.

3. El subproceso está esperando un objeto, que a su vez está esperando permanentemente a que se complete (como llamar al método de espera de este objeto), porque otros subprocesos siempre se despiertan continuamente.

¿Cuál es el principio de actualización de las cerraduras multiproceso?

En Java, hay cuatro estados de bloqueo, los niveles son de menor a mayor: bloqueos sin estado, bloqueos sesgados, bloqueos ligeros y estados de bloqueo pesados. Estos estados aumentarán gradualmente con la competencia. Las cerraduras se pueden actualizar pero no degradar

AQ (nivel.) S (AbstractQueuedSynchronizer) explicación detallada y análisis del código fuente

Introducción a AQS

El nombre completo de AQS (AbstractQueuedSynchronizer), esta clase se encuentra en el paquete java.util.concurrent.locks.

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo de enlace antirrobo, se recomienda guardar la imagen y cargarla directamente (img-bMVDUy0x-1692509038142) (04-Preguntas de la entrevista de programación concurrente (última versión de 2020) - focus.assets/image-20201109202104977 .png)]

AQS es un marco para construir bloqueos y sincronizadores. AQS se puede utilizar para construir de manera fácil y eficiente una gran cantidad de sincronizadores ampliamente utilizados, como ReentrantLock y Semaphore que mencionamos, y otros como ReentrantReadWriteLock, SynchronousQueue, FutureTask, etc. basado en AQS. Por supuesto, también podemos usar AQS para construir fácilmente un sincronizador que satisfaga nuestras propias necesidades.

Análisis de principios de AQS

La mayor parte del siguiente contenido se proporciona en las notas de clase de AQS, pero es un poco más difícil de leer en inglés. Si está interesado, puede consultar el código fuente.

Descripción general de los principios de AQS

La idea central de AQS es que si el recurso compartido solicitado está inactivo, el subproceso que actualmente solicita el recurso se configura como un subproceso de trabajo efectivo y el recurso compartido se establece en un estado bloqueado. Si el recurso compartido solicitado está ocupado, se requiere un mecanismo para bloquear subprocesos y esperar y bloquear la asignación cuando se despierta.

AQS se implementa con bloqueos de cola CLH, es decir, los subprocesos que no pueden adquirir bloqueos temporalmente se agregan a la cola.

La cola CLH (Craig, Landin y Hagersten) es una cola virtual bidireccional (una cola virtual bidireccional significa que no hay una instancia de cola, solo la relación de asociación entre nodos). AQS encapsula cada hilo que solicita recursos compartidos en un nodo (Nodo) de una cola de bloqueo CLH para implementar la asignación de bloqueo.

Mire un esquema de AQS (AbstractQueuedSynchronizer):

[Falló la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-ObkxhaOx-1692509038142) (04-Preguntas de la entrevista de programación concurrente (última versión de 2020) -focus .assets/image-20201109202131975 .png)]

AQS utiliza una variable miembro int para representar el estado de sincronización y completa el trabajo de cola del subproceso de recursos a través de la cola FIFO incorporada. AQS utiliza CAS para realizar operaciones atómicas en el estado de sincronización para modificar su valor.

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

La información de estado se opera a través del tipo protegido getState, setState, compareAndSetState

1	//返回同步状态的当前值
2	protected final int getState() {
3	return state;
4	}
5	// 设置同步状态的值
6	protected final void setState(int newState) {
7	state = newState;
8	}
9	//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect (期望值)
10	protected final boolean compareAndSetState(int expect, int update) { 
11 return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
12 }

Cómo AQS comparte recursos

AQS define dos métodos para compartir recursos

  • xclusive (exclusivo): solo se puede ejecutar un subproceso, como ReentrantLock. Se puede dividir en bloqueo justo y bloqueo injusto:

    Bloqueo justo: según el orden de los subprocesos en la cola, el primero en llegar obtiene el bloqueo primero

    Bloqueo injusto: cuando un hilo quiere adquirir un candado, ignora el orden de la cola y toma el candado directamente. Quien lo tome será el propietario.

  • Compartir (compartido): se pueden ejecutar varios subprocesos simultáneamente, como Semaphore/CountDownLatch. Hablaremos de Semaphore, CountDownLatch, CyclicBarrier y ReadWriteLock más adelante. ReentrantReadWriteLock puede considerarse como una combinación, porque ReentrantReadWriteLock también es un bloqueo de lectura y escritura que permite que varios subprocesos lean un recurso al mismo tiempo. Diferentes sincronizadores personalizados compiten por recursos compartidos de diferentes maneras. Cuando se implementa el sincronizador personalizado, solo necesita implementar el método de adquisición y liberación del estado del recurso compartido, en cuanto al mantenimiento de la cola de espera de subprocesos específicos (como poner en cola/despertar fuera de la cola después de no poder adquirir recursos, etc. .), AQS ya se ha implementado al más alto nivel.

La capa inferior de AQS utiliza el patrón del método de plantilla. El diseño del sincronizador se basa en el patrón del método de plantilla. Si necesita personalizar el sincronizador, la forma general es esta

(Una aplicación clásica del patrón del método de plantilla):

  1. Los usuarios heredan AbstractQueuedSynchronizer y anulan los métodos especificados. (Estos métodos de reescritura son muy simples, nada más que adquirir y liberar el estado de los recursos compartidos)

  2. Combine AQS en la implementación de un componente de sincronización personalizado y llame a sus métodos de plantilla, que a su vez llaman a métodos anulados por el usuario.

Esto es muy diferente de la forma en que solíamos implementar interfaces en el pasado. Esta es una aplicación clásica del patrón de método de plantilla.

AQS utiliza el patrón de método de plantilla. Al personalizar el sincronizador, debe reescribir los siguientes métodos de plantilla proporcionados por AQS:

1	isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
2	tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
3	tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
4	tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
5	tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回fals e。
6

Cada método arroja una excepción UnsupportedOperationException de forma predeterminada. Las implementaciones de estos métodos deben ser internamente seguras para subprocesos y, en general, deben ser breves en lugar de bloqueantes. Otros métodos en la clase AQS son finales, por lo que otras clases no pueden usarlos, solo estos métodos pueden ser usados ​​por otras clases. Tomando ReentrantLock como ejemplo, el estado se inicializa a 0, lo que indica el estado desbloqueado. Cuando un hilo se bloquea (),

Llame a tryAcquire() para monopolizar el bloqueo y el estado+1. Después de eso, otros hilos intentarán adquirir()

Fallará y otros subprocesos tendrán la oportunidad de adquirir el bloqueo hasta que el desbloqueo del subproceso A () alcance el estado = 0 (es decir, se libere el bloqueo). Por supuesto, antes de que se libere el bloqueo, el subproceso A puede adquirir el bloqueo repetidamente (el estado estará cansado)

Además), este es el concepto de reentrada. Pero preste atención a cuántas veces necesita liberarlo para garantizar que el estado pueda volver a cero.

Tomando CountDownLatch como ejemplo, la tarea se divide en N subprocesos para ejecutar y el estado también se inicializa como

N (tenga en cuenta que N debe ser coherente con el número de subprocesos). Los N subprocesos se ejecutan en paralelo. Después de ejecutar cada subproceso, cuenta atrás () una vez y el estado disminuirá en 1 en CAS (Comparar e intercambiar). Después de que se ejecuten todos los subprocesos (es decir, estado = 0), el subproceso de llamada principal será unpark (), y luego el subproceso de llamada principal regresará de la función await () para continuar con las acciones restantes.

En términos generales, los sincronizadores personalizados son métodos exclusivos o métodos compartidos y solo necesitan implementar uno de tryAcquire-tryRelease y tryAcquireShared-tryReleaseShared. Sin embargo, AQS también admite sincronizadores personalizados para implementar métodos exclusivos y compartidos al mismo tiempo, como

ReentrantReadWriteLock。

El principio de implementación de ReentrantLock (bloqueo reentrante) es diferente del bloqueo justo y el bloqueo injusto ¿Qué es un bloqueo reentrante (ReentrantLock)?

ReentrantLock es una clase que implementa la interfaz Lock y también se usa con frecuencia en la programación real.

Un bloqueo con una tasa alta que admite la reentrada, lo que significa que los recursos compartidos se pueden bloquear repetidamente, es decir, el hilo actual no se bloqueará si adquiere el bloqueo nuevamente.

La palabra clave de Java sincronizada admite implícitamente la reentrada, y sincronizada logra la reentrada adquiriendo incremento automático y liberando decremento automático. Al mismo tiempo, ReentrantLock también admite bloqueos justos y bloqueos injustos. Entonces, si quieres entender completamente ReentrantLock, lo principal es

Aprendizaje de la semántica de sincronización de ReentrantLock: 1. El principio de realización de la reentrada 2. Bloqueo justo y bloqueo injusto.

El principio de implementación de la reentrada Para admitir la reentrada, se deben resolver dos problemas: 1. Cuando un hilo adquiere un bloqueo, si el hilo que ya adquirió el bloqueo es el hilo actual, lo adquirirá directamente nuevamente con éxito; 2. Dado que el bloqueo se adquiere n veces, se considera que el bloqueo se libera por completo solo después de que se libera el mismo n veces.

ReentrantLock admite dos tipos de bloqueos: bloqueos justos y bloqueos injustos. Lo que es equidad se refiere a la adquisición de candados. Si un candado es justo, el orden en que se adquieren los candados debe ajustarse al orden de tiempo absoluto de la solicitud y satisfacer FIFO.

Análisis del código fuente del bloqueo de lectura y escritura ReentrantReadWriteLock

¿Qué es ReadWriteLock?

En primer lugar, dejemos claro que no es que ReentrantLock sea malo, sino que ReentrantLock a veces tiene limitaciones. Si usa ReentrantLock, puede ser para evitar la inconsistencia de datos causada por el hilo A escribiendo datos y el hilo B leyendo datos, pero de esta manera, si el hilo C está leyendo datos y el hilo D también está leyendo datos, la lectura de datos no cambiará los datos. , no es necesario bloquear, pero aún así está bloqueado, lo que reduce el rendimiento del programa. Debido a esto, nació el bloqueo de lectura y escritura ReadWriteLock.

ReadWriteLock es una interfaz de bloqueo de lectura y escritura, y el bloqueo de lectura y escritura es una separación de bloqueo que se utiliza para mejorar el rendimiento de programas concurrentes.

La tecnología ReentrantReadWriteLock es una implementación específica de la interfaz ReadWriteLock, que realiza la separación de lectura y escritura. El bloqueo de lectura es compartido y el bloqueo de escritura es exclusivo. No hay exclusión mutua entre lectura y lectura. Leer y escribir, escribir y leer, escribir y escribir son mutuamente excluyentes, lo que mejora el rendimiento de lectura y escritura.

El bloqueo de lectura y escritura tiene las siguientes tres características importantes:

(1) Selectividad justa: admite métodos de adquisición de bloqueo injustos (predeterminados) y justos, y el rendimiento sigue siendo mejor que justo.

(2) Reingreso: tanto los bloqueos de lectura como los de escritura admiten el reingreso de subprocesos.

(3) Degradación del bloqueo: siguiendo la secuencia de adquirir un bloqueo de escritura, adquirir un bloqueo de lectura y luego liberar un bloqueo de escritura, un bloqueo de escritura se puede degradar a un bloqueo de lectura.

Análisis del código fuente de condición y mecanismo de notificación de espera LockSupport explicación detallada del contenedor concurrente ConcurrentHashMap explicación detallada del contenedor concurrente (versión JDK1.8) y análisis del código fuente

¿Qué es ConcurrentHashMap?

ConcurrentHashMap es una implementación de HashMap eficiente y segura para subprocesos en Java. Por lo general, implica una alta concurrencia. Si desea utilizar la estructura del mapa, es lo primero que le viene a la mente. En comparación con hashmap, ConcurrentHashMap es un mapa seguro para subprocesos y utiliza la idea de segmentación de bloqueo para mejorar la concurrencia.

Entonces, ¿cómo se logra exactamente la seguridad de los subprocesos?

Elementos clave de la versión JDK 1.6:

  • El segmento hereda la función de ReentrantLock como bloqueo y proporciona garantía de seguridad de subprocesos para cada segmento;
  • El segmento mantiene varios depósitos de la tabla hash y cada depósito es una lista vinculada compuesta por HashEntry.

Después de JDK1.8, ConcurrentHashMap abandonó el bloqueo de segmento original y adoptó

CAS+ sincronizado para garantizar la seguridad de la concurrencia.

¿Cuál es la concurrencia de ConcurrentHashMap en Java?

ConcurrentHashMap divide el mapa real en varias partes para lograr su escalabilidad y seguridad para subprocesos. Esta división se obtiene mediante concurrencia, que es el constructor de la clase ConcurrentHashMap.

Un parámetro opcional para , con un valor predeterminado de 16, para que se pueda evitar la contención en situaciones de subprocesos múltiples.

Después de JDK8, abandonó el concepto de Segmento (segmento de bloqueo), pero permitió una nueva forma de implementarlo, utilizando el algoritmo CAS. Al mismo tiempo, se agregan más variables auxiliares para mejorar la concurrencia. Para más detalles, consulte el código fuente.

¿Cuál es la implementación de contenedores concurrentes?

¿Qué es un contenedor de sincronización? Puede entenderse simplemente como un contenedor que logra la sincronización a través de sincronizado, si

Hay varios subprocesos que llaman a métodos del contenedor sincronizado y se ejecutarán en serie. Por ejemplo, Vector,

Hashtable y los contenedores devueltos por Collections.synchronizedSet, sincronizadoList y otros métodos. Al ver los códigos de implementación de contenedores de sincronización como Vector y Hashtable, puede ver que la forma en que estos contenedores logran la seguridad de los subprocesos es encapsular sus estados y agregar la palabra clave sincronizada a los métodos que deben sincronizarse.

El contenedor concurrente utiliza una estrategia de bloqueo completamente diferente del contenedor sincrónico para proporcionar una mayor concurrencia y escalabilidad. Por ejemplo, en ConcurrentHashMap se adopta un mecanismo de bloqueo más fino, que se puede llamar bloqueo segmentado. En este mecanismo de bloqueo, cualquier Se permite que una cantidad determinada de subprocesos de lectura accedan al mapa simultáneamente, y los subprocesos que realizan operaciones de lectura y los subprocesos que escriben operaciones también pueden acceder al mapa simultáneamente, al tiempo que se permite un cierto número de operaciones de escritura.

Los subprocesos de trabajo modifican el mapa al mismo tiempo, por lo que pueden lograr un mayor rendimiento en un entorno concurrente.

¿Cuál es la diferencia entre colecciones sincronizadas y concurrentes en Java?

Tanto las colecciones sincronizadas como las colecciones concurrentes proporcionan colecciones seguras para subprocesos adecuadas para subprocesos múltiples y concurrencia, pero las colecciones concurrentes son más escalables. Antes de Java 1.5, los programadores solo tenían que usar colecciones sincronizadas, lo que causaba contención cuando varios subprocesos eran simultáneos, lo que dificultaba la escalabilidad del sistema. Java5 introdujo colecciones concurrentes como

ConcurrentHashMap no solo proporciona seguridad para subprocesos, sino que también mejora la escalabilidad con tecnologías modernas como la separación de bloqueos y la partición interna.

¿Cuál es la diferencia entre SynchronizedMap y ConcurrentHashMap?

SynchronizedMap bloquea toda la tabla a la vez para garantizar la seguridad de los subprocesos, de modo que solo un subproceso pueda acceder al mapa a la vez.

ConcurrentHashMap utiliza bloqueos de segmento para garantizar el rendimiento en subprocesos múltiples.

En ConcurrentHashMap, se bloquea un depósito a la vez. ConcurrentHashMap divide la tabla hash en 16 depósitos de forma predeterminada, y las operaciones comunes como obtener, colocar y eliminar solo bloquean los depósitos que se necesitan actualmente.

De esta manera, antes solo podía ingresar un subproceso, pero ahora se pueden ejecutar 16 subprocesos de escritura al mismo tiempo y la mejora del rendimiento de concurrencia es obvia.

Además, ConcurrentHashMap utiliza un método de iteración diferente. En este método de iteración, cuando se crea el iterador y la colección cambia, ya no se lanza.

ConcurrentModificationException, reemplazada por nuevos datos nuevos al cambiar para no afectar los datos originales, una vez que se completa el iterador, reemplace el puntero principal con nuevos datos, para que el hilo del iterador pueda usar los datos antiguos originales y el hilo de escritura también pueda ser concurrente Complete el cambio.

Explicación detallada de CopyOnWriteArrayList de contenedores concurrentes

¿Qué es CopyOnWriteArrayList y para qué escenarios de aplicación se puede utilizar? ¿Cuáles son los pros y los contras?

CopyOnWriteArrayList es un contenedor concurrente. Mucha gente lo llama seguro para subprocesos. Creo que esta oración no es rigurosa y carece de un requisito previo, es decir, es seguro para subprocesos para operar en escenarios no compuestos.

Uno de los beneficios de CopyOnWriteArrayList (contenedor sin bloqueo) es que cuando varios iteradores atraviesan y modifican la lista al mismo tiempo, no se generará ConcurrentModificationException. existir

En CopyOnWriteArrayList, la escritura hará que se cree una copia de toda la matriz subyacente, mientras que la matriz de origen permanecerá en su lugar, lo que hace que las operaciones de lectura sean seguras mientras se modifica la matriz copiada.

Los escenarios de uso de CopyOnWriteArrayList se analizan a través del código fuente y podemos ver que sus ventajas y desventajas son más obvias, por lo que los escenarios de uso son más obvios. Es adecuado para escenarios en los que se lee más y se escribe menos.

Desventajas de CopyOnWriteArrayList

  1. Debido a que la matriz debe copiarse durante la operación de escritura, consumirá memoria. Si la matriz original tiene mucho contenido, puede generar un gc joven o un gc completo.

  2. No se puede utilizar para escenarios de lectura en tiempo real, como copiar matrices y agregar nuevos elementos. Lleva tiempo, por lo que después de llamar a una operación de conjunto, es posible que los datos leídos aún sean antiguos, aunque

CopyOnWriteArrayList puede lograr la coherencia final, pero aún no puede cumplir con los requisitos en tiempo real.

  1. Debido a que en el uso real, es posible que no sea posible garantizar la cantidad de datos que se colocarán en CopyOnWriteArrayList. Si hay demasiados datos, la matriz debe copiarse cada vez que se agrega/configura, lo cual es demasiado costoso. En las aplicaciones de Internet de alto rendimiento, estas operaciones provocan fallos cada minuto.

Idea de diseño de CopyOnWriteArrayList

  1. Separación de lectura y escritura, lectura y escritura separadas.

  2. consistencia eventual

  3. Utilice otra idea para abrir espacio para resolver conflictos de concurrencia: ThreadLocal explica detalladamente los contenedores concurrentes

¿Qué es ThreadLocal? ¿Cuáles son los escenarios de uso?

hreadLocal es una clase de herramienta de copia de variable de subproceso local, que crea una

Objeto ThreadLocalMap, en pocas palabras, ThreadLocal es una forma de intercambiar espacio por tiempo.

Cada hilo puede acceder al valor en su propio objeto interno ThreadLocalMap. De esta forma, se evita el intercambio de recursos entre múltiples subprocesos.

Principio: las variables locales del hilo son variables limitadas al hilo en sí, que pertenecen al hilo en sí y no se comparten entre varios hilos. Java proporciona la clase ThreadLocal para admitir variables locales de subprocesos, que es una forma de lograr la seguridad de los subprocesos. Pero tenga cuidado al utilizar variables locales de subprocesos en un entorno administrado (como un servidor web), donde el subproceso de trabajo sobrevive a cualquier variable de la aplicación.

Cualquier variable local de subproceso que no se libere una vez finalizado el trabajo pone a la aplicación Java en riesgo de sufrir pérdidas de memoria.

El escenario de uso clásico es asignar una conexión JDBC para cada subproceso. De esta manera, se puede garantizar que cada subproceso realice operaciones de base de datos en su propia Conexión, y no habrá problemas como que el subproceso A cierre la Conexión que está utilizando el subproceso B; también hay problemas como la gestión de sesiones. Ejemplo de uso de ThreadLocal:

public class TestThreadLocal {
2
3	//线程本地存储变量
4	private static final ThreadLocal<Integer> THREAD_LOCAL_NUM
5	= new ThreadLocal<Integer>() {
6	@Override
7	protected Integer initialValue() {
8	return 0;
9	}
10	};
11
12	public static void main(String[] args) {
13	for (int i = 0; i <3; i++) {//启动三个线程
14	Thread t = new Thread() {
15	@Override
16	public void run() {
17	add10ByThreadLocal();
18	}
19	};
20	t.start();
21	}
22	}
23
24	/**
25	* 线程本地存储变量加 5
*/
26 
27	private static void add10ByThreadLocal() {
28	for (int i = 0; i <5; i++) {
29	Integer n = THREAD_LOCAL_NUM.get();
30	n += 1;
31	THREAD_LOCAL_NUM.set(n);
32	System.out.println(Thread.currentThread().getName() + " : ThreadLocal n um=" + n);
33	}
34	}
35
36 }

Resultados de la impresión: se inician 3 subprocesos y, después de cada subproceso, se imprime en "ThreadLocal num=5", en lugar de acumular num hasta que el valor sea igual a 15.

1	Thread‐0 : ThreadLocal num=1
2	Thread‐1 : ThreadLocal num=1
3	Thread‐0 : ThreadLocal num=2
4	Thread‐0 : ThreadLocal num=3
5	Thread‐1 : ThreadLocal num=2
6	Thread‐2 : ThreadLocal num=1
7	Thread‐0 : ThreadLocal num=4
8	Thread‐2 : ThreadLocal num=2
9	Thread‐1 : ThreadLocal num=3
10	Thread‐1 : ThreadLocal num=4
11	Thread‐2 : ThreadLocal num=3
12	Thread‐0 : ThreadLocal num=5
13	Thread‐2 : ThreadLocal num=4
14	Thread‐2 : ThreadLocal num=5
15	Thread‐1 : ThreadLocal num=5

¿Qué son las variables locales de hilo?

Una variable local de subproceso es una variable limitada al subproceso en sí, que pertenece al subproceso en sí y no se comparte entre varios subprocesos. Java proporciona la clase ThreadLocal para admitir variables locales de subprocesos, que es una forma de lograr la seguridad de los subprocesos. Pero tenga cuidado al utilizar variables locales de subprocesos en un entorno administrado (como un servidor web), donde el subproceso de trabajo sobrevive a cualquier variable de la aplicación. Cualquier variable local de subproceso que no se libere una vez finalizado el trabajo pone a la aplicación Java en riesgo de sufrir pérdidas de memoria.

Análisis y solución de pérdida de memoria ThreadLocal

¿Qué hace que ThreadLocal provoque pérdidas de memoria?

La clave utilizada en ThreadLocalMap es una referencia débil a ThreadLocal, mientras que el valor es una referencia fuerte. Por lo tanto, si no se hace una fuerte referencia a ThreadLocal externamente, la clave se limpiará durante la recolección de basura, pero el valor no se limpiará. De esta forma, aparecerá una Entrada cuya clave es nula en ThreadLocalMap. Si no tomamos ninguna medida, GC nunca recuperará el valor y puede ocurrir una pérdida de memoria en este momento. Esta situación se ha considerado en la implementación de ThreadLocalMap: cuando se llaman los métodos set (), get () y remove (), se limpiarán los registros cuya clave sea nula. terminar de usar

Después del método ThreadLocal, es mejor llamar manualmente al método remove()

¿Solución de pérdida de memoria ThreadLocal?

  • Cada vez que se utiliza ThreadLocal, se llama a su método remove() para borrar los datos.
  • En el caso de utilizar el grupo de subprocesos, si ThreadLocal no se limpia a tiempo, no solo es un problema de pérdida de memoria, sino que, lo que es más grave, puede causar problemas con la lógica empresarial. Por lo tanto, usar ThreadLocal es lo mismo que desbloquear después de bloquear y limpiar después de su uso.

Explicación detallada de BlockingQueue para contenedores concurrentes

¿Qué es una cola de bloqueo? ¿Cuál es el principio de implementación de la cola de bloqueo? ¿Cómo utilizar la cola de bloqueo para implementar el modelo productor-consumidor?

Una cola de bloqueo (BlockingQueue) es una cola que admite dos operaciones adicionales.

Estas dos operaciones adicionales son: cuando la cola está vacía, el hilo que obtiene el elemento espera a que la cola deje de estar vacía. Cuando la cola está llena, el hilo que almacena el elemento espera a que la cola esté disponible.

Las colas de bloqueo se utilizan a menudo en escenarios de productores y consumidores: el productor es el hilo que agrega elementos a la cola y el consumidor es el hilo que toma elementos de la cola. Una cola de bloqueo es un contenedor donde los productores almacenan elementos y los consumidores solo toman elementos del contenedor.

JDK7 proporciona 7 colas de bloqueo. Ellos son:

ArrayBlockingQueue: una cola de bloqueo delimitada compuesta por estructuras de matriz.

LinkedBlockingQueue: una cola de bloqueo limitada compuesta por una estructura de lista vinculada. PriorityBlockingQueue: una cola de bloqueo ilimitada que admite la clasificación por prioridad.

DelayQueue: una cola de bloqueo ilimitada implementada mediante una cola de prioridad.

SynchronousQueue: una cola de bloqueo que no almacena elementos.

LinkedTransferQueue: una cola de bloqueo ilimitada compuesta por una estructura de lista vinculada. LinkedBlockingDeque: una cola de bloqueo bidireccional compuesta por una estructura de lista vinculada.

Al implementar el acceso sincrónico antes de Java 5, puede usar una colección ordinaria y luego usar la cooperativa de subprocesos.

La operación y la sincronización de subprocesos pueden realizar los modos productor y consumidor, la tecnología principal es utilizarla bien.

Espere, notifique, notifique a todos, sincronice estas palabras clave. Después de Java 5, se puede implementar mediante el uso de colas de bloqueo, lo que reduce en gran medida la cantidad de código, facilita la programación multiproceso y garantiza la seguridad.

La interfaz BlockingQueue es una subinterfaz de Queue, su propósito principal no es servir como contenedor, sino

Es una herramienta para la sincronización de subprocesos, por lo que tiene una característica obvia: cuando el subproceso productor intenta colocar elementos en BlockingQueue, si la cola está llena, el subproceso se bloquea. Cuando el subproceso consumidor intenta sacar un elemento de él , Si la cola está vacía, el hilo se bloqueará. Es precisamente debido a esta característica que varios hilos en el programa alternativamente colocan elementos en BlockingQueue y sacan elementos. Puede controlar muy bien la comunicación entre hilos.

El escenario clásico de usar una cola de bloqueo es leer y analizar los datos del cliente de socket. El subproceso que lee los datos los coloca continuamente en la cola y luego el subproceso de análisis obtiene continuamente datos de la cola para su análisis.

Explicación detallada de ConcurrentLinkedQueue y análisis del código fuente de contenedores concurrentes Explicación detallada de ArrayBlockingQueue y LinkedBlockingQueue en contenedores concurrentes Los ejecutores del grupo de subprocesos crean cuatro grupos de subprocesos comunes ¿Qué es un grupo de subprocesos? ¿Cuáles son las formas de crearlo?

La tecnología de agrupación no es infrecuente en comparación con todos: los grupos de subprocesos, los grupos de conexiones de bases de datos, los grupos de conexiones Http, etc., son aplicaciones de esta idea. La idea de agrupar tecnología es principalmente reducir el consumo de cada adquisición de recursos y mejorar la utilización de los recursos.

En la programación orientada a objetos, crear y destruir objetos lleva mucho tiempo, porque crear un objeto requiere adquirir recursos de memoria u otros recursos adicionales. Aún más en Java, la máquina virtual intentará realizar un seguimiento de cada objeto para que pueda ser recolectado como basura después de que se destruya el objeto. Por lo tanto, una forma de mejorar la eficiencia de los programas de servicio es reducir al máximo el número de creación y destrucción de objetos, especialmente la creación y destrucción de algunos objetos que consumen muchos recursos, razón por la cual se utiliza la tecnología de "agrupación de recursos".

Como su nombre lo indica, el grupo de subprocesos consiste en crear varios subprocesos ejecutables con anticipación y colocarlos en un grupo (contenedor). Cuando sea necesario, los subprocesos se pueden obtener del grupo sin crearlos usted mismo. Después de su uso, los subprocesos no necesitan para ser destruido pero devuelto al grupo, lo que reduce la necesidad de creación y procesamiento La sobrecarga de destruir objetos de hilo. La interfaz Executor en Java 5+ define una función para ejecutar subprocesos. Su subtipo, la interfaz del grupo de subprocesos, es ExecutorService. Es más complicado configurar un grupo de subprocesos, especialmente cuando el principio del grupo de subprocesos no es muy claro, por lo que en la clase de herramienta

Executors proporciona algunos métodos de fábrica estáticos para generar algunos grupos de subprocesos de uso común, de la siguiente manera:

(1) newSingleThreadExecutor: crea un grupo de subprocesos de un solo subproceso. En este grupo de subprocesos solo hay un subproceso funcionando, lo que equivale a ejecutar todas las tareas en serie con un solo subproceso. Si el único hilo termina de forma anormal, un nuevo hilo ocupará su lugar. Este grupo de subprocesos garantiza que el orden de ejecución de todas las tareas se ejecute en el orden en que se envían las tareas.

(2) newFixedThreadPool: crea un grupo de subprocesos de tamaño fijo. Se crea un subproceso cada vez que se envía una tarea hasta que el subproceso alcanza el tamaño máximo del grupo de subprocesos. Una vez que el tamaño del grupo de subprocesos alcanza un valor grande, el

Permanecerá sin cambios; si un subproceso finaliza debido a una excepción de ejecución, el grupo de subprocesos agregará un nuevo subproceso. Si desea utilizar el grupo de subprocesos en el servidor, se recomienda utilizar el método newFixedThreadPool para crear un grupo de subprocesos, que puede lograr un mejor rendimiento.

(3) newCachedThreadPool: crea un grupo de subprocesos que se puede almacenar en caché. Si el tamaño del grupo de subprocesos excede la cantidad de subprocesos necesarios para procesar las tareas, se recuperarán algunos subprocesos inactivos (que no ejecutan tareas durante 60 segundos). Cuando aumenta la cantidad de tareas, el grupo de subprocesos puede agregar de manera inteligente nuevos subprocesos para procesar las tareas. . Este grupo de subprocesos no limita el tamaño del grupo de subprocesos, que depende completamente del tamaño del subproceso más grande que el sistema operativo (o JVM) puede crear.

(4) newScheduledThreadPool: crea un grupo de subprocesos con tamaño ilimitado. Este grupo de subprocesos admite la sincronización y la ejecución periódica de tareas.

¿Cuáles son las ventajas del grupo de subprocesos?

  • Reduzca el consumo de recursos: reutilice los subprocesos existentes y reduzca la sobrecarga de creación y destrucción de objetos.
  • Mejorar la capacidad de respuesta. Puede controlar eficazmente la cantidad de subprocesos simultáneos, mejorar la tasa de utilización de los recursos del sistema y evitar la competencia y el bloqueo excesivos de los recursos. Cuando llega una tarea, la tarea se puede ejecutar inmediatamente sin esperar a que se cree el hilo.
  • Mejorar la capacidad de gestión de subprocesos. Los subprocesos son recursos escasos. Si se crean sin límite, no solo consumirán recursos del sistema, sino que también reducirán la estabilidad del sistema. El uso del grupo de subprocesos se puede utilizar para la asignación, el ajuste y el monitoreo unificados.
  • Funciones adicionales: proporciona funciones como ejecución temporizada, ejecución periódica, subproceso único y control de concurrencia. En resumen, el uso del marco del grupo de subprocesos Executor puede administrar mejor los subprocesos y mejorar la utilización de los recursos del sistema.

¿Cuáles son los estados del grupo de subprocesos?

  • EN EJECUCIÓN: este es el estado normal, acepta nuevas tareas y procesa tareas en la cola de espera. APAGADO: no acepta envíos de nuevas tareas, pero continuará procesando tareas en la cola de espera.
  • DETENER: no acepte nuevos envíos de tareas, ya no procese tareas en la cola de espera e interrumpa los subprocesos que están ejecutando tareas.
  • ORDENAR: Todas las tareas se destruyen, workCount es 0 y el estado del grupo de subprocesos está cambiando a
  • En el estado ORDENANDO, se ejecutará el método de enlace terminado ().
  • TERMINADO: Una vez finalizado el método terminado (), el estado del grupo de subprocesos será este.

¿Qué es el marco ejecutor? ¿Por qué utilizar el marco Ejecutor?

El marco Ejecutor es un marco para tareas asincrónicas que se invocan, programan, ejecutan y controlan de acuerdo con un conjunto de políticas de ejecución.

Crear un subproceso nuevo Thread () cada vez que se ejecuta una tarea consume más rendimiento. Crear un subproceso requiere mucho tiempo y recursos, y la creación de subprocesos ilimitados provocará un desbordamiento de la memoria de la aplicación.

Por lo tanto, crear un grupo de subprocesos es una mejor solución, porque puede limitar la cantidad de subprocesos y puede

Recicla y reutiliza estos hilos. Es muy conveniente crear un grupo de subprocesos utilizando el marco de Ejecutores.

¿Diferencia entre Ejecutor y Ejecutores en Java?

  • Los diferentes métodos de la clase de herramienta Executors crean diferentes grupos de subprocesos según nuestras necesidades para satisfacer las necesidades comerciales.
  • Los objetos de la interfaz ejecutor pueden ejecutar nuestras tareas de hilo.
  • La interfaz ExecutorService hereda y amplía la interfaz Executor, proporcionándonos más métodos para obtener el estado de ejecución de la tarea y el valor de retorno de la tarea.
  • Utilice ThreadPoolExecutor para crear grupos de subprocesos personalizados.
  • Future representa el resultado de un cálculo asincrónico y proporciona un método para verificar si el cálculo se completa, esperar a que se complete y usar el método get () para obtener el resultado del cálculo.

¿Cuál es la diferencia entre los métodos de envío () y ejecución () en el grupo de subprocesos?

Parámetros recibidos: ejecutar() solo puede ejecutar tareas de tipo Runnable. enviar() se puede ejecutar

Tareas de tipo Runnable y Callable.

Valor de retorno: el método enviar() puede devolver un objeto Futuro que contiene el resultado del cálculo, mientras que ejecutar() no tiene manejo de excepciones: enviar() es conveniente para el manejo de excepciones

¿Qué es un grupo de subprocesos y por qué está en desuso en Java?

La clase ThreadGroup puede asignar subprocesos a un determinado grupo de subprocesos. Puede haber objetos de subprocesos en el grupo de subprocesos y también puede haber grupos de subprocesos. También puede haber subprocesos en el grupo. Esta estructura organizativa es algo similar a la forma de un árbol.

El grupo de subprocesos y el grupo de subprocesos son dos conceptos diferentes, sus funciones son completamente diferentes: el primero es facilitar la gestión de subprocesos, mientras que el segundo es gestionar el ciclo de vida de los subprocesos, reutilizar subprocesos y reducir la sobrecarga de creación y destrucción de subprocesos. . ¿Por qué los grupos de hilos están en desuso? Debido a que existen muchos riesgos de seguridad al usarlo, no existe una investigación específica. Si necesita usarlo, se recomienda utilizar el grupo de subprocesos.

Explicación detallada de ThreadPoolExecutor en Thread Pool

La diferencia entre Executors y ThreaPoolExecutor para crear un grupo de subprocesos

El "Manual de desarrollo de Java de Alibaba" exige que no se permita la creación de grupos de subprocesos mediante Ejecutores, sino a través de ThreadPoolExecutor. Este método de procesamiento hace que los estudiantes que escriben tengan más claridad sobre las reglas de ejecución del grupo de subprocesos y evita el riesgo de agotamiento de recursos.

Desventajas de cada método de Ejecutores:

  • newFixedThreadPool y newSingleThreadExecutor:
    el principal problema es que la cola de procesamiento de solicitudes acumuladas puede consumir una gran cantidad de memoria, o incluso OOM.
  • newCachedThreadPool y newScheduledThreadPool:
    el principal problema es que el número máximo de subprocesos es Integer.MAX_VALUE, lo que puede crear una gran cantidad de subprocesos, o incluso OOM.

ThreaPoolExecutor solo tiene una forma de crear un grupo de subprocesos: utilizar su constructor y especificar los parámetros usted mismo.

¿Sabes cómo crear un grupo de subprocesos?

Hay muchas formas de crear un grupo de subprocesos, aquí solo necesita responder ThreadPoolExecutor.

ThreadPoolExecutor () es la creación original del grupo de subprocesos y también es una forma claramente regulada de crear grupos de subprocesos en el Manual de desarrollo de Java de Alibaba.

Análisis de parámetros importantes del constructor ThreadPoolExecutor

Los tres parámetros más importantes de ThreadPoolExecutor:

corePoolSize: la cantidad de subprocesos principales, la cantidad de subprocesos define la cantidad de subprocesos que se pueden ejecutar al mismo tiempo.

MaximumPoolSize: el número máximo de subprocesos de trabajo que se permite que existan en el grupo de subprocesos.

workQueue: cuando llega una nueva tarea, primero juzgará si el número de subprocesos actualmente en ejecución alcanza el número de subprocesos principales y, de ser así, la tarea se almacenará en la cola.

Otros parámetros comunes de ThreadPoolExecutor:

  1. keepAliveTime: cuando la cantidad de subprocesos en el grupo de subprocesos es mayor que corePoolSize, si no se envía una nueva tarea en este momento, los subprocesos fuera del subproceso principal no se destruirán inmediatamente, sino que esperarán hasta que el tiempo de espera exceda keepAliveTime antes de ser reciclado y destruido;

  2. unidad: la unidad de tiempo para el parámetro keepAliveTime.

  3. threadFactory: proporciona una fábrica de subprocesos para que el grupo de subprocesos cree nuevos subprocesos.

  4. controlador: la política de rechazo después de que la cola de tareas del grupo de subprocesos excede la política de saturación máxima de PoolSize ThreadPoolExecutor

Definición de la política de saturación de ThreadPoolExecutor:

Si la cantidad de subprocesos que se ejecutan actualmente al mismo tiempo alcanza la cantidad máxima de subprocesos y la cola está llena,

ThreadPoolTaskExecutor define algunas estrategias:

  • ThreadPoolExecutor.AbortPolicy: lanza RejectedExecutionException para rechazar el procesamiento de nuevas tareas.
  • ThreadPoolExecutor.CallerRunsPolicy: Llame para ejecutar sus propias tareas de ejecución de subprocesos. No realizarás solicitudes de tareas. Pero esta estrategia reducirá la velocidad de envío de nuevas tareas y afectará el rendimiento general del programa. Además, a esta estrategia le gusta aumentar la capacidad de la cola. Puede elegir esta estrategia si su aplicación puede tolerar este retraso y no puede descartar ninguna solicitud de tarea.
  • ThreadPoolExecutor.DiscardPolicy: no procese nuevas tareas, simplemente deséchelas.
  • ThreadPoolExecutor.DiscardOldestPolicy: esta política descartará solicitudes de tareas más antiguas sin procesar.

Por ejemplo: cuando Spring crea un grupo de subprocesos a través de ThreadPoolTaskExecutor o directamente a través del constructor de ThreadPoolExecutor, cuando no especificamos

La estrategia de saturación RejectedExecutionHandler se utiliza de forma predeterminada al configurar el grupo de subprocesos.

ThreadPoolExecutor.AbortPolicy. De forma predeterminada, ThreadPoolExecutor lanzará RejectedExecutionException para rechazar nuevas tareas, lo que significa que perderá el procesamiento de esta tarea. Para aplicaciones escalables, se recomienda utilizar

ThreadPoolExecutor.CallerRunsPolicy. Cuando el grupo grande está lleno, esta estrategia nos proporciona

Cola de escala. (Esto se puede ver directamente mirando el código fuente del constructor de ThreadPoolExecutor. Por una razón relativamente simple, el código no se publicará aquí).

Una demostración simple del grupo de subprocesos: principio de implementación del grupo de subprocesos Runnable + ThreadPoolExecutor

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo de enlace antirrobo, se recomienda guardar la imagen y cargarla directamente (img-IUxHoZtN-1692509038143) (04-Preguntas de la entrevista de programación concurrente (última versión de 2020) - focus.assets/image-20201109203453876 .png)]

Para que todos sean más conscientes de algunos de los conceptos de las preguntas de la entrevista anteriores, escribí un grupo de hilos simple.

Manifestación.

Primero cree una clase de implementación de la interfaz Runnable (por supuesto, también puede ser la interfaz Callable, también mencionamos la diferencia entre las dos anteriores).

1 import java.util.Date;
2
3/** 
4  * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
5  */
6 public class MyRunnable implements Runnable {
7 
8  private String command;
9 
10  public MyRunnable(String s) { 
11  this.command = s;
12  }
13
14  @Override 
15  public void run() { 
16  System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); 
17  processCommand();
18  System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); 
19  }
20 
21  private void processCommand() {
22  try {
23  Thread.sleep(5000);
24  } catch (InterruptedException e) {
25  e.printStackTrace(); 
26  } 
27  }
28 
29  @Override
30  public String toString() { 
31  return this.command;
32  }
33 } 

Para escribir un programa de prueba, aquí creamos un grupo de subprocesos utilizando los parámetros personalizados del constructor ThreadPoolExecutor recomendado por Alibaba.

1 import java.util.concurrent.ArrayBlockingQueue; 
2 import java.util.concurrent.ThreadPoolExecutor;
3 import java.util.concurrent.TimeUnit;
4 
5 public class ThreadPoolExecutorDemo {
6
7  private static final int CORE_POOL_SIZE = 5;
8  private static final int MAX_POOL_SIZE = 10; 
9  private static final int QUEUE_CAPACITY = 100;
10  private static final Long KEEP_ALIVE_TIME = 1L; 
11  public static void main(String[] args) {
12
13  //使用阿里巴巴推荐的创建线程池的方式
14  //通过ThreadPoolExecutor构造函数自定义参数创建 
15  ThreadPoolExecutor executor = new ThreadPoolExecutor( 
16  CORE_POOL_SIZE, 
17  MAX_POOL_SIZE, 
18  KEEP_ALIVE_TIME, 
19  TimeUnit.SECONDS,
20  new ArrayBlockingQueue<>(QUEUE_CAPACITY), 
21  new ThreadPoolExecutor.CallerRunsPolicy());
22
23  for (int i = 0; i < 10; i++) { 
24  //创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
25  Runnable worker = new MyRunnable("" + i); 
26  //执行Runnable 
27  executor.execute(worker);
28  } 
29  //终止线程池 
30  executor.shutdown(); 
31  while (!executor.isTerminated()) { 
32  } 
33  System.out.println("Finished all threads");
34  }
35 } 

Puede ver que nuestro código anterior especifica:

  1. corePoolSize: el número de subprocesos principales es 5.

  2. MaximumPoolSize: número máximo de subprocesos 10

  3. keepAliveTime: El tiempo de espera es 1L.

  4. unidad: La unidad de tiempo de espera es TimeUnit.SECONDS.

  5. workQueue: la cola de tareas es ArrayBlockingQueue con una capacidad de 100;

controlador: La política de saturación es CallerRunsPolicy. Producción:

1	pool‐1‐thread‐2 Start. Time = Tue Nov 12 20:59:44 CST 2019
2	pool‐1‐thread‐5 Start. Time = Tue Nov 12 20:59:44 CST 2019
3	pool‐1‐thread‐4 Start. Time = Tue Nov 12 20:59:44 CST 2019
4	pool‐1‐thread‐1 Start. Time = Tue Nov 12 20:59:44 CST 2019
5	pool‐1‐thread‐3 Start. Time = Tue Nov 12 20:59:44 CST 2019
6	pool‐1‐thread‐5 End. Time = Tue Nov 12 20:59:49 CST 2019
7	pool‐1‐thread‐3 End. Time = Tue Nov 12 20:59:49 CST 2019
8	pool‐1‐thread‐2 End. Time = Tue Nov 12 20:59:49 CST 2019
9	pool‐1‐thread‐4 End. Time = Tue Nov 12 20:59:49 CST 2019
10	pool‐1‐thread‐1 End. Time = Tue Nov 12 20:59:49 CST 2019
11	pool‐1‐thread‐2 Start. Time = Tue Nov 12 20:59:49 CST 2019
12	pool‐1‐thread‐1 Start. Time = Tue Nov 12 20:59:49 CST 2019
13	pool‐1‐thread‐4 Start. Time = Tue Nov 12 20:59:49 CST 2019
14	pool‐1‐thread‐3 Start. Time = Tue Nov 12 20:59:49 CST 2019
15	pool‐1‐thread‐5 Start. Time = Tue Nov 12 20:59:49 CST 2019
16	pool‐1‐thread‐2 End. Time = Tue Nov 12 20:59:54 CST 2019
17	pool‐1‐thread‐3 End. Time = Tue Nov 12 20:59:54 CST 2019
18	pool‐1‐thread‐4 End. Time = Tue Nov 12 20:59:54 CST 2019
19	pool‐1‐thread‐5 End. Time = Tue Nov 12 20:59:54 CST 2019
20	pool‐1‐thread‐1 End. Time = Tue Nov 12 20:59:54 CST 2019

Explicación detallada de ScheduledThreadPoolExecutor del grupo de subprocesos FutureTask explicación detallada de la clase de operación atómica

¿Qué son las operaciones atómicas? ¿Cuáles son las clases atómicas en la API de concurrencia de Java?

Operación atómica (operación atómica) significa "una o una serie de operaciones que no pueden interrumpirse".

El procesador utiliza un método basado en bloquear el caché o bloquear el bus para realizar operaciones atómicas entre múltiples procesadores.

En Java, las operaciones atómicas se pueden realizar mediante bloqueos y CAS circular. Operación CAS——

Comparar y configurar, o Comparar e intercambiar, ahora casi todas las instrucciones de la CPU son compatibles

Operación atómica de CAS.

Una operación atómica es una unidad de tarea operativa que no se ve afectada por otras operaciones. Las operaciones atómicas son necesarias para evitar la inconsistencia de los datos en un entorno de subprocesos múltiples.

int++ no es una operación atómica, por lo que cuando un hilo lee su valor y agrega 1, otro hilo puede leer el valor anterior, lo que provocará un error.

Para resolver este problema, es necesario garantizar que la operación de aumento sea atómica. Antes de JDK1.5, podemos usar

Tecnología de sincronización para hacer esto. A partir de JDK1.5, el paquete java.util.concurrent.atomic proporciona clases contenedoras atómicas para los tipos int y long, que garantizan automáticamente que sus operaciones sean atómicas y no requieran el uso de sincronización.

El paquete java.util.concurrent proporciona un conjunto de clases atómicas. Su característica básica es que en un entorno multiproceso, cuando varios subprocesos ejecutan los métodos contenidos en las instancias de estas clases al mismo tiempo, son exclusivos, es decir, cuando un subproceso ingresa al método y ejecuta las instrucciones en él, no será interrumpido por otros subprocesos Interrupción, mientras que otros subprocesos son como bloqueos de giro, esperando hasta que se complete la ejecución del método, y luego la JVM selecciona otro subproceso para ingresar desde la cola de espera. Esto es solo una comprensión lógica.

Clases atómicas: AtomicBoolean, AtomicInteger, AtomicLong,

Referencia Atómica

Matrices atómicas: AtomicIntegerArray, AtomicLongArray,

Matriz de referencia atómica

Actualizadores de propiedades atómicas: AtomicLongFieldUpdater, AtomicIntegerFieldUpdater,

Actualizador de campo de referencia atómica

Una clase atómica que resuelve el problema ABA: AtomicMarkableReference (al introducir un

booleano para reflejar si ha cambiado en el medio), AtomicStampedReference (al introducir un int para acumular para reflejar si ha cambiado en el medio) ¿cuénteme sobre el principio de atómico?

La característica básica de las clases en el paquete Atomic es que en un entorno de subprocesos múltiples, cuando varios subprocesos simultáneamente

(Incluidos los tipos básicos y los tipos de referencia) las variables son exclusivas cuando se operan, es decir, cuando varios subprocesos actualizan el valor de la variable al mismo tiempo, solo un subproceso puede tener éxito y el subproceso fallido puede ser lo mismo que un bloqueo de giro. Continúe intentándolo hasta que la ejecución sea exitosa.

Parte del código fuente de la clase AtomicInteger:

1	// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
2	private static final Unsafe unsafe = Unsafe.getUnsafe(); 3 private static final long valueOffset;
4
5	static {
6	try {
7	valueOffset = unsafe.objectFieldOffset
8	(AtomicInteger.class.getDeclaredField("value"));
9	} catch (Exception ex) { throw new Error(ex); }
10	}
11
12 private volatile int value;

La clase AtomicInteger utiliza principalmente CAS (comparar e intercambiar) + métodos volátiles y nativos para garantizar operaciones atómicas, evitando así la alta sobrecarga de sincronización y mejorando en gran medida la eficiencia de ejecución. El principio de CAS es comparar el valor esperado con un valor original y actualizarlo a un nuevo valor si son iguales.

El método objectFieldOffset () de la clase UnSafe es un método local que se utiliza para obtener la dirección de memoria del "valor original" y el valor de retorno es valueOffset. Además el valor es un

Una variable volátil es visible en la memoria, por lo que la JVM puede garantizar que cualquier hilo siempre pueda obtener el nuevo valor de la variable en cualquier momento.

herramientas de concurrencia

CountDownLatch y CyclicBarrier de herramientas concurrentes

¿Cuál es la diferencia entre CycliBarriar y CountdownLatch en Java?

Tanto CountDownLatch como CyclicBarrier son clases de herramientas utilizadas para controlar la concurrencia, y ambas pueden entenderse como mantener un contador, pero las dos aún tienen énfasis diferentes:

  • CountDownLatch se usa generalmente para que un determinado subproceso A espere a que varios otros subprocesos ejecuten tareas antes de ejecutarse; mientras que CyclicBarrier generalmente se usa para que un grupo de subprocesos esperen entre sí hasta un cierto estado, y luego este grupo de subprocesos se ejecuta en al mismo tiempo; CountDownLatch enfatiza un hilo Espere a que varios hilos completen algo. CyclicBarrier es que varios subprocesos son iguales entre sí y, una vez que todos hayan terminado, trabajarán juntos.
  • Después de llamar al método countDown de CountDownLatch, el hilo actual no se bloqueará y continuará ejecutándose, mientras que llamar al método de espera de CyclicBarrier bloqueará el hilo actual hasta que todos los hilos especificados por CyclicBarrier alcancen el punto especificado antes de continuar.
  • El método CountDownLatch es relativamente pequeño y la operación es relativamente simple, mientras que CyclicBarrier proporciona más métodos, como getNumberWaiting (), isBroken (). Estos métodos pueden obtener el estado de varios subprocesos actuales y se pueden pasar al método de construcción de CyclicBarrier. barrierAction, especificando cuando todos La función comercial se ejecuta cuando llegan todos los subprocesos;
  • CountDownLatch no se puede reutilizar, pero CyclicLatch se puede reutilizar.

Semáforo e Intercambiador de herramientas concurrentes

¿Qué hace un semáforo?

El semáforo es un semáforo y su función es limitar el número de concurrencia de un determinado bloque de código.

Semaphore tiene un constructor que puede pasar un entero n, lo que indica que solo se puede acceder a un determinado fragmento de código mediante un máximo de n subprocesos. Si excede n, espere hasta que un determinado subproceso termine de ejecutar este bloque de código y el siguiente hilo Vuelve a entrar. Se puede ver a partir de esto que si el entero int n = 1 pasado en el constructor de Semaphore es equivalente a sincronizarse.

Semáforo (semáforo): permite el acceso de varios subprocesos al mismo tiempo: sincronizado y

ReentrantLock solo permite que un subproceso acceda a un determinado recurso a la vez, y Semaphore (semáforo) puede especificar que varios subprocesos accedan a un determinado recurso al mismo tiempo.

¿Qué es una herramienta Exchanger para intercambiar datos entre subprocesos?

Exchanger es una clase de herramienta para la colaboración entre subprocesos, que se utiliza para intercambiar datos entre dos subprocesos. proporciona

Se establece un punto de sincronización de intercambio en el que dos subprocesos pueden intercambiar datos. El intercambio de datos se logra a través del método de intercambio. Si un subproceso ejecuta el método de intercambio primero, esperará a que el otro subproceso también ejecute el método de intercambio. En este momento, ambos subprocesos han alcanzado el punto de sincronización y los dos subprocesos pueden intercambiar. datos. .

¿Cuáles son las clases de herramientas de concurrencia más utilizadas?

  • Semaphore (semaphore): permite el acceso de varios subprocesos al mismo tiempo: tanto sincronizado como ReentrantLock permiten que solo un subproceso acceda a un determinado recurso a la vez, y Semaphore (semaphore) puede especificar varios subprocesos para acceder a un determinado recurso al mismo tiempo. .
  • CountDownLatch (temporizador de cuenta regresiva): CountDownLatch es una clase de herramienta de sincronización que se utiliza para coordinar la sincronización entre múltiples subprocesos. Esta herramienta se usa generalmente para controlar la espera de subprocesos y puede hacer que un determinado subproceso espere hasta que finalice la cuenta regresiva antes de comenzar la ejecución.
  • CyclicBarrier (valla de ciclo): CyclicBarrier es muy similar a CountDownLatch. También puede implementar esperas técnicas entre subprocesos, pero su función es más compleja y poderosa que CountDownLatch. Los principales escenarios de aplicación son similares a CountDownLatch. CyclicBarrier significa literalmente barrera (Barrera) reciclable (cíclica). Lo que tiene que hacer es bloquear un grupo de subprocesos cuando llegan a una barrera (también llamada punto de sincronización), y la barrera no se abrirá hasta que el siguiente subproceso llegue a la barrera, y todos los subprocesos bloqueados por la barrera seguirán funcionando. . El método de construcción predeterminado de CyclicBarrier es CyclicBarrier (int Parties), su parámetro indica el número de subprocesos interceptados por la barrera, cada subproceso llama al método await() para informar
  • CyclicBarrier He alcanzado la barrera, luego el hilo actual está bloqueado.

La variable ile es visible en la memoria, por lo que la JVM puede garantizar que cualquier hilo siempre pueda obtener el nuevo valor de la variable en cualquier momento.

herramientas de concurrencia

CountDownLatch y CyclicBarrier de herramientas concurrentes

¿Cuál es la diferencia entre CycliBarriar y CountdownLatch en Java?

Tanto CountDownLatch como CyclicBarrier son clases de herramientas utilizadas para controlar la concurrencia, y ambas pueden entenderse como mantener un contador, pero las dos aún tienen énfasis diferentes:

  • CountDownLatch se usa generalmente para que un determinado subproceso A espere a que varios otros subprocesos ejecuten tareas antes de ejecutarse; mientras que CyclicBarrier generalmente se usa para que un grupo de subprocesos esperen entre sí hasta un cierto estado, y luego este grupo de subprocesos se ejecuta en al mismo tiempo; CountDownLatch enfatiza un hilo Espere a que varios hilos completen algo. CyclicBarrier es que varios subprocesos son iguales entre sí y, una vez que todos hayan terminado, trabajarán juntos.
  • 调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行;
  • CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入 barrierAction,指定当所有线程都到达时执行的业务功能;
  • CountDownLatch是不能复用的,而CyclicLatch是可以复用的。

并发工具之Semaphore与Exchanger

Semaphore 有什么作用

Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。

Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码 多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。

Semaphore(信号量)-允许多个线程同时访问: synchronized 和

ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量) 可以指定多个线程同时访问某个资源。

什么是线程间交换数据的工具Exchanger

Exchanger是一个用于线程间协作的工具类,用于两个线程间交换数据。它提供

了一个交换的同步点,在这个同步点两个线程能够交换数据。交换数据是通过 exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据。

常用的并发工具类有哪些?

  • Semaphore (semaphore): permite el acceso de varios subprocesos al mismo tiempo: tanto sincronizado como ReentrantLock permiten que solo un subproceso acceda a un determinado recurso a la vez, y Semaphore (semaphore) puede especificar varios subprocesos para acceder a un determinado recurso al mismo tiempo. .
  • CountDownLatch (temporizador de cuenta regresiva): CountDownLatch es una clase de herramienta de sincronización que se utiliza para coordinar la sincronización entre múltiples subprocesos. Esta herramienta se usa generalmente para controlar la espera de subprocesos y puede hacer que un determinado subproceso espere hasta que finalice la cuenta regresiva antes de comenzar la ejecución.
  • CyclicBarrier (valla de ciclo): CyclicBarrier es muy similar a CountDownLatch. También puede implementar esperas técnicas entre subprocesos, pero su función es más compleja y poderosa que CountDownLatch. Los principales escenarios de aplicación son similares a CountDownLatch. CyclicBarrier significa literalmente barrera (Barrera) reciclable (cíclica). Lo que tiene que hacer es bloquear un grupo de subprocesos cuando llegan a una barrera (también llamada punto de sincronización), y la barrera no se abrirá hasta que el siguiente subproceso llegue a la barrera, y todos los subprocesos bloqueados por la barrera seguirán funcionando. . El método de construcción predeterminado de CyclicBarrier es CyclicBarrier (int Parties), su parámetro indica el número de subprocesos interceptados por la barrera, cada subproceso llama al método await() para informar
  • CyclicBarrier He alcanzado la barrera, luego el hilo actual está bloqueado.

practica concurrente

Supongo que te gusta

Origin blog.csdn.net/leader_song/article/details/132391098
Recomendado
Clasificación