[Edición de coleccionista] 80 Preguntas de entrevista clásicas de simultaneidad multiproceso de Java

prefacio

Colección personal de 80 preguntas de entrevistas clásicas de subprocesos múltiples/concurrencia de Java, porque el espacio es demasiado largo, ahora daré 1-10 respuestas y las analizaré, y las mejoraré juntas más tarde, y las subiré a github ~

github.com/whx123/Java… ❞

1. ¿El principio de implementación de la optimización sincronizada y de bloqueo?

El principio de realización de sincronizado.

  • Actos sincronizados en "métodos" o "bloques de código" , asegurando que solo un subproceso pueda acceder al código modificado a la vez.
  • Cuando sincronizado modifica un bloque de código, la JVM usa dos instrucciones "monitorenter, monitorexit" para lograr la sincronización
  • Cuando sincronizado modifica un método de sincronización, la JVM usa la etiqueta "ACC_SYNCHRONIZED" para lograr la sincronización
  • monitorenter, monitorexit o ACC_SYNCHRONIZED son implementaciones "basadas en monitores "
  • El objeto de instancia tiene un encabezado de objeto, el encabezado de objeto tiene Marcar palabra y el puntero Marcar palabra apunta a "supervisar"
  • Monitor es en realidad una "herramienta de sincronización" , que también se puede decir que es un "mecanismo de sincronización" .
  • En la máquina virtual Java (HotSpot), Monitor se implementa mediante "ObjectMonitor" . ObjectMonitor refleja el principio de funcionamiento de Monitor~
ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录线程获取锁的次数
    _waiters      = 0,
    _recursions   = 0;  //锁的重入次数
    _object       = NULL;
    _owner        = NULL;  // 指向持有ObjectMonitor对象的线程
    _WaitSet      = NULL;  // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
复制代码

Varios atributos clave de ObjectMonitor _count, _recursions, _owner, _WaitSet, _EntryList reflejan el principio de funcionamiento del monitor

optimización de bloqueo

Antes de discutir la optimización de bloqueo, echemos un vistazo al diagrama de estructura de Mark Word en el encabezado del objeto JAVA (JVM de 32 bits) ~

Mark Word almacena los datos operativos del objeto en sí, como "código hash, edad de generación de GC, indicador de estado de bloqueo, marca de tiempo sesgada (Epoch)" , etc., ¿por qué distinguir entre "bloqueo sesgado, bloqueo ligero, bloqueo pesado" y así? en el tipo de estado de bloqueo?

Antes de JDK1.6, la implementación de sincronizado llamaba directamente a la entrada y salida de ObjectMonitor, y este tipo de bloqueo se denominaba "bloqueo pesado". A partir de JDK6, el equipo de desarrollo de máquinas virtuales de HotSpot ha optimizado los bloqueos en Java, como agregar estrategias de optimización como el giro adaptativo, la eliminación de bloqueos, el engrosamiento de bloqueos, los bloqueos livianos y los bloqueos sesgados.

  • Bloqueo sesgado: en ausencia de competencia, se elimina toda la sincronización y no se realiza la operación CAS.
  • Bloqueo ligero: cuando no hay competencia multiproceso, es un bloqueo relativamente pesado y reduce el consumo de rendimiento causado por la exclusión mutua del sistema operativo. Sin embargo, si hay contención de bloqueo, además de la sobrecarga del mutex en sí, también existe la sobrecarga de las operaciones CAS.
  • Spinlocks: reduce los cambios de contexto de CPU innecesarios. Cuando un candado liviano se actualiza a un candado pesado, se usa el método de bloqueo giratorio
  • Engrosamiento de bloqueo: conecte varias operaciones de bloqueo y desbloqueo consecutivas para expandirse en un bloqueo más grande.

Por ejemplo, comprar un boleto para ingresar al zoológico. El maestro lleva a un grupo de niños a visitar. Si el revisor de boletos sabe que son un grupo, puede considerarlos como un todo (alquiler bloqueado) y revisar los boletos a la vez, en lugar de pedirles que revisen los boletos uno por uno. .

  • Eliminación de bloqueos: cuando se ejecuta el compilador justo a tiempo de la máquina virtual, se requiere sincronizar algunos códigos, pero se detecta que no hay posibilidad de contención de datos compartidos.

Los amigos interesados ​​pueden leer mi artículo: Análisis sincronizado: si está dispuesto a pelar mi corazón capa por capa [1]

2. Principio de ThreadLocal, precauciones de uso y cuáles son los escenarios de aplicación.

Responda cuatro puntos principales:

  • ¿Qué es ThreadLocal?
  • Principio ThreadLocal
  • Notas sobre el uso de ThreadLocal
  • Escenarios de aplicaciones de ThreadLocal

¿Qué es ThreadLocal?

ThreadLocal, la variable local del hilo. Si crea una variable ThreadLocal, cada subproceso que acceda a esta variable tendrá una copia local de la variable. Cuando varios subprocesos operan esta variable, en realidad operan las variables en su propia memoria local, logrando así el aislamiento del subproceso.Función para evitar la seguridad del subproceso cuestiones.

//创建一个ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
复制代码

Principio ThreadLocal

Diagrama de estructura de memoria ThreadLocal:

Se puede ver en el diagrama de estructura:

  • El objeto Thread contiene una variable miembro de ThreadLocal.ThreadLocalMap.
  • ThreadLocalMap mantiene internamente una matriz de Entry, cada Entry representa un objeto completo, la clave es ThreadLocal en sí misma y el valor es el valor genérico de ThreadLocal.

Es más fácil de entender comparando estos códigos fuente clave ~

public class Thread implements Runnable {
   //ThreadLocal.ThreadLocalMap是Thread的属性
   ThreadLocal.ThreadLocalMap threadLocals = null;
}
复制代码

Los métodos clave set() y get() en ThreadLocal

    public void set(T value) {
        Thread t = Thread.currentThread(); //获取当前线程t
        ThreadLocalMap map = getMap(t);  //根据当前线程获取到ThreadLocalMap
        if (map != null)
            map.set(this, value); //K,V设置到ThreadLocalMap中
        else
            createMap(t, value); //创建一个新的ThreadLocalMap
    }

    public T get() {
        Thread t = Thread.currentThread();//获取当前线程t
        ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
        if (map != null) {
            //由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value; 
                return result;
            }
        }
        return setInitialValue();
    }
复制代码

Matriz de entrada de ThreadLocalMap

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}
复制代码

Entonces, ¿cómo responder "el principio de implementación de ThreadLocal" ? De la siguiente manera, es mejor combinar el diagrama de estructura anterior para explicar juntos~

❝ La clase Thread tiene una variable de instancia threadLocals de tipo ThreadLocal.ThreadLocalMap, es decir, cada hilo tiene su propio ThreadLocalMap. ThreadLocalMap mantiene un arreglo de Entry, cada Entry representa un objeto completo, la clave es ThreadLocal mismo, y el valor es The valor genérico de ThreadLocal, cuando cada hilo establece un valor en ThreadLocal, lo almacena en su propio ThreadLocalMap, y al leer, utiliza un ThreadLocal como referencia, y encuentra la clave correspondiente en su propio mapa, realizando así la Cuarentena del hilo. ❞

Problema de pérdida de memoria de ThreadLocal

Echemos un vistazo al diagrama de referencia de TreadLocal,

La clave utilizada en ThreadLocalMap es la referencia débil de ThreadLocal, de la siguiente manera

Referencia débil: siempre que se ejecute el mecanismo de recolección de basura, independientemente de si el espacio de memoria de la JVM es suficiente, se recuperará la memoria ocupada por el objeto.

Las referencias débiles son más fáciles de reciclar. Por lo tanto, si ThreadLocal (Clave de ThreadLocalMap) es reciclado por el recolector de basura, pero debido a que el ciclo de vida de ThreadLocalMap es el mismo que el de Thread, si no se recicla en este momento, sucederá esto: la clave de ThreadLocalMap se ha ido , y el valor todavía está allí Ahora, esto "crea un problema de pérdida de memoria" .

¿Cómo "resolver el problema de pérdida de memoria" ? Después de usar ThreadLocal, llame al método remove() a tiempo para liberar espacio en la memoria.

Escenarios de aplicaciones de ThreadLocal

  • grupo de conexiones de base de datos
  • utilizado en la gestión de sesiones

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

Recuerdo que cuando la escuela estaba reclutando, la frecuencia de esta pregunta de la entrevista era bastante alta. Puede responder esta pregunta desde varias dimensiones, como la implementación de bloqueos, las características funcionales y el rendimiento.

  • "Implementación de bloqueos:" Sincronizado es una palabra clave en el lenguaje Java y se implementa en base a la JVM. ReentrantLock se implementa en función del nivel de API de JDK (por lo general, los métodos lock() y unlock() se completan con bloques de instrucciones try/finally).
  • "Rendimiento:" antes de la optimización de bloqueo de JDK1.6, el rendimiento sincronizado era mucho peor que el de ReenTrantLock. Sin embargo, desde JDK6, se han agregado giros adaptativos, eliminación de bloqueos, etc., y el rendimiento de los dos es similar.
  • "Características:" ReentrantLock agrega algunas funciones avanzadas a las sincronizadas, como la espera de notificaciones interrumpibles, de bloqueo justo y selectivas.

❝ ReentrantLock proporciona un mecanismo que puede interrumpir subprocesos que esperan bloqueos. Este mecanismo se implementa a través de lock.lockInterruptably(). ReentrantLock puede especificar si es un bloqueo justo o injusto. Y sincronizado solo puede ser un bloqueo injusto. El so- llamado bloqueo justo es El subproceso que espera primero obtiene el bloqueo primero. Sincronizado se combina con los métodos esperar () y notificar ()/notificar a todos () para implementar el mecanismo de espera/notificación. La clase ReentrantLock se implementa por medio de la interfaz de condición y el método newCondition(). ReentrantLock debe declararse manualmente para bloquear y liberar Los bloqueos generalmente se liberan junto con finalmente. Sin embargo, sincronizado no necesita liberar el bloqueo manualmente. ❞

4. Hable sobre la diferencia entre CountDownLatch y CyclicBarrier

  • CountDownLatch: uno o más subprocesos, esperando que otros subprocesos completen algo antes de ejecutar;
  • CyclicBarrier: Múltiples subprocesos esperan el uno al otro hasta que alcanzan el mismo punto de sincronización y luego continúan ejecutándose juntos.

Por ejemplo:

❝ CountDownLatch: suponga que el maestro y los compañeros de clase acordaron reunirse en la puerta del parque el fin de semana y luego emitir los boletos cuando todos estén listos. Luego, para emitir los boletos (este hilo principal), debe esperar a que todos los antes de la ejecución. .CyclicBarrier: Varios velocistas comenzarán la competencia de atletismo, y solo cuando todos los atletas estén listos, el árbitro disparará el arma y comenzará, y todos los atletas estarán galopando en este momento.❞

5. Comprensión del marco Fork/Join

El marco Fork/Join es un marco proporcionado por Java7 para ejecutar tareas en paralelo.Es un marco que divide una tarea grande en varias tareas pequeñas y finalmente resume los resultados de cada tarea pequeña para obtener los resultados de la tarea grande.

El marco Fork/Join necesita comprender dos puntos, "divide y vencerás" y "algoritmo de robo de trabajo" .

"Divide y conquistaras"

La definición anterior del marco Fork/Join es la encarnación de la idea de divide y vencerás.

"Algoritmos de robo de trabajo"

Divida las tareas grandes en tareas pequeñas, colóquelas en diferentes colas para su ejecución y entréguelas a diferentes subprocesos para su ejecución. Algunos subprocesos han completado las tareas de las que son responsables primero, y otros subprocesos todavía están procesando lentamente sus propias tareas. En este momento, para mejorar completamente la eficiencia, se necesita un algoritmo de robo de trabajo ~

El algoritmo de robo de trabajo es "el proceso por el cual un subproceso roba tareas de otras colas para su ejecución" . En general, significa que el hilo rápido (robo de hilo) toma la tarea del hilo lento. Al mismo tiempo, para reducir la competencia de bloqueo, se suele utilizar una cola de dos extremos, es decir, el hilo rápido y el lento. hilo están en un extremo.

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

Mire la descripción del método de inicio de Thread~

    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
    public synchronized void start() {
     ......
    }
复制代码

Cuando la JVM ejecuta el método de inicio, iniciará un nuevo subproceso para ejecutar el método de ejecución del subproceso, que tiene el efecto de subprocesos múltiples ~ "¿Por qué no podemos llamar directamente al método run()?" Si llama directamente al El método run() de Thread, el método todavía se está ejecutando en el hilo principal, no hay efecto de subprocesos múltiples.

7. ¿CAS? ¿Qué tiene de malo CAS y cómo solucionarlo?

CAS, Compare and Swap, compare e intercambie;

CAS involucra 3 operandos, el valor de la dirección de memoria V, el valor original esperado A y el nuevo valor B; si el valor V de la ubicación de la memoria coincide con el valor A original esperado, se actualiza al nuevo valor B, de lo contrario es no actualizado

¿Qué tiene de malo CAS?

"El problema ABA"

En un entorno concurrente, suponiendo que la condición inicial es A, al modificar los datos se encuentra que A ejecutará la modificación. Sin embargo, aunque sea A, puede haber una situación en la que A cambie a B y B vuelva a cambiar a A. En este momento, A ya no es A, e incluso si los datos se modifican con éxito, puede haber problemas.

El problema de ABA se puede "resolver" mediante AtomicStampedReference , que, una clase de referencia atómica etiquetada, garantiza la corrección de CAS al controlar la versión de los valores de las variables.

"Sobrecarga de tiempo de ciclo largo"

Spin CAS, si se ha ejecutado en un bucle, no ha tenido éxito, traerá una gran sobrecarga de ejecución a la CPU.

Muchas veces, la idea de CAS refleja que hay una serie de giros, solo para evitar este problema que consume mucho tiempo ~

"Solo operaciones atómicas garantizadas en una variable".

CAS garantiza la atomicidad de las operaciones realizadas en una variable Actualmente, CAS no puede garantizar directamente la atomicidad de las operaciones cuando se opera en múltiples variables.

Este problema se puede resolver de dos formas:

❝ Use mutex para garantizar la atomicidad; encapsule múltiples variables en objetos y use AtomicReference para garantizar la atomicidad.❞

Los amigos interesados ​​pueden echar un vistazo a mi artículo práctico anterior ha ~ Una práctica de bloqueo optimista CAS para resolver problemas de concurrencia [2]

9. ¿Cómo garantizar el resultado correcto de i++ en subprocesos múltiples?

  • Uso de CAS cíclico para lograr operaciones atómicas i ++
  • Uso del mecanismo de bloqueo para implementar operaciones atómicas i++
  • Use sincronizado para implementar operaciones atómicas i ++

Sin demostración de código, se siente sin alma ~ de la siguiente manera:

/**
 *  @
 */
public class AtomicIntegerTest {

    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        testIAdd();
    }

    private static void testIAdd() throws InterruptedException {
        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(() -> {
                for (int j = 0; j < 2; j++) {
                    //自增并返回当前值
                    int andIncrement = atomicInteger.incrementAndGet();
                    System.out.println("线程:" + Thread.currentThread().getName() + " count=" + andIncrement);
                }
            });
        }
        executorService.shutdown();
        Thread.sleep(100);
        System.out.println("最终结果是 :" + atomicInteger.get());
    }
    
}
复制代码

resultado de la operación:

...
线程:pool-1-thread-1 count=1997
线程:pool-1-thread-1 count=1998
线程:pool-1-thread-1 count=1999
线程:pool-1-thread-2 count=315
线程:pool-1-thread-2 count=2000
最终结果是 :2000
复制代码

10. ¿Cómo detectar interbloqueos? ¿Cómo prevenir el punto muerto? Cuatro condiciones necesarias para el interbloqueo

Interbloqueo se refiere a un punto muerto en el que múltiples subprocesos esperan el uno al otro debido a la competencia de recursos. Siéntelo así:

"Cuatro condiciones necesarias para el interbloqueo:"

  • Exclusión mutua: solo un proceso puede usar un recurso a la vez. Otros procesos no pueden acceder a los recursos que se han asignado a otros procesos.
  • Ocupar y esperar: cuando un proceso está esperando que se asignen otros recursos, continúa ocupando los recursos asignados.
  • No preventivo: no puede adelantar a la fuerza los recursos que ya están ocupados por un proceso.
  • Espera circular: existe una cadena de procesos cerrada de modo que cada recurso ocupa al menos un recurso requerido por el siguiente proceso de la cadena.

"¿Cómo prevenir el punto muerto?"

  • Orden de bloqueo (los subprocesos funcionan en orden)
  • Límite de tiempo de bloqueo (el permiso agregado por la solicitud del hilo, el tiempo de espera se abandonará y el bloqueo ocupado por sí mismo se liberará al mismo tiempo)
  • detección de punto muerto

referencia y gracias

Newton dijo, la razón por la que veo lejos es porque me paro sobre los hombros de gigantes ~ Gracias, los siguientes mayores ~

  • ¿Entiende el CAS que se debe preguntar en la entrevista? [3]
  • Subprocesamiento múltiple de Java: Interbloqueo [4]
  • Resumen de bloqueos de reentrada ReenTrantLock (la diferencia con sincronizados) [5]
  • Charla sobre concurrencia (8) - Introducción al marco Fork/Join [6]


Autor: niño recogiendo caracoles
Enlace: https://juejin.cn/post/6854573221258199048
Fuente: Rare Earth Nuggets
Los derechos de autor pertenecen al autor. Para reimpresiones comerciales, comuníquese con el autor para obtener autorización, y para reimpresiones no comerciales, indique la fuente.

Supongo que te gusta

Origin blog.csdn.net/wdjnb/article/details/124323297
Recomendado
Clasificación