Análisis del código fuente de Java y preguntas de la entrevista: análisis del código fuente de LinkedBlockingQueue

Esta serie de blog relacionado, Mu columna de referencia de clase Java de código fuente y del sistema fabricantes entrevistador sucinta Zhenti
por debajo de esta columna es la dirección de GitHub:
Fuente resolvió: https://github.com/luanqiu/java8
artículo Demostración: HTTPS: // GitHub. com / luanqiu / java8_demo los
compañeros de clase pueden verlo si es necesario)

Análisis del código fuente de Java y preguntas de la entrevista: análisis del código fuente de LinkedBlockingQueue

Palabras introductorias
Cuando se trata de colas, la reacción de todos puede ser que nunca lo he usado. Debería ser una API sin importancia. Si lo crees, es un gran error. Usualmente usamos grupos de hilos, bloqueos de lectura y escritura, colas de mensajes y otras tecnologías y marcos. Los principios subyacentes son las colas, por lo que no debemos subestimar las colas. La base de la API y el aprendizaje de las colas son muy importantes para aprender Java en profundidad.

Este artículo toma principalmente la cola LinkedBlockingQueue como un ejemplo para describir la implementación específica de la capa inferior en detalle.

1 Arquitectura general

LinkedBlockingQueue Chinese se denomina cola de bloqueo de listas vinculadas. Este nombre es muy bueno. Desde el nombre, se sabe que la estructura de datos subyacente es una lista vinculada, y la cola es bloqueable. A continuación, veremos LinkedBlockingQueue desde la estructura general.

1.1 Diagrama de clases

Primero, echemos un vistazo al diagrama de clase LinkedBlockingQueue, de la siguiente manera:
Inserte la descripción de la imagen aquí
Desde el diagrama de clase, probablemente podamos ver dos caminos:

  1. AbstractQueue-> AbstractCollection-> Collection-> Iterable Esta dependencia de ruta es principalmente para reutilizar algunas operaciones de Collection e iterator. Cuando hablamos de colecciones, todos sabemos lo que hacen estas clases y lo que podemos hacer, por lo que no elaboramos más. Sobre
  2. BlockingQueue-> Queue-> Collection, BlockingQueue y Queue son dos interfaces nuevas, concentrémonos en ello.

La cola es la interfaz más básica. Casi todas las clases de implementación de cola implementarán esta interfaz. Esta interfaz define tres tipos de operaciones para colas:
nuevas operaciones:

  1. Se produce una excepción cuando la cola de agregar está llena;
  2. falso cuando la cola de oferta está llena.
    Ver y eliminar operaciones:
  3. Se produce una excepción cuando la cola de eliminación está vacía;
  4. Cuando la cola de la encuesta está vacía, devuelve nulo.
    Ver solo sin eliminar:
  5. Se produce una excepción cuando la cola del elemento está vacía;
  6. Cuando la cola de espera está vacía, se devuelve un valor nulo.

Un total de 6 métodos, además de los métodos de clasificación anteriores, también se pueden dividir en dos categorías:

  1. Cuando la cola está llena o vacía, lanza una excepción, como agregar, quitar, elemento;
  2. Cuando la cola está llena o vacía, devuelve valores especiales, como oferta, encuesta, vistazo.

De hecho, estos son más difíciles de recordar. Cada vez que necesite usarlo, miraré el código fuente para recordar si este método arroja una excepción o devuelve un valor especial.

BlockingQueue agrega el concepto de bloqueo en base a Queue, como bloquear todo el tiempo o bloquear por un período de tiempo. Para facilitar la memoria, dibujamos una tabla de la siguiente manera:

Operación Lanzar una excepción Valor especial Seguir bloqueando Bloquear por un rato
Nueva cola de operaciones llena añadir oferta devuelve falso poner La oferta vuelve falsa después del tiempo de espera
Ver y eliminar operaciones-cola vacía eliminar la encuesta devuelve nulo tomar la encuesta devuelve nulo después del tiempo de espera
Solo ver y no eliminar operaciones: la cola está vacía elemento vistazo devuelve nulo No No

PD: El método remove, como se define en la anotación de la clase BlockingQueue, genera una excepción, pero el método remove en LinkedBlockingQueue en realidad devuelve falso.

Como puede ver en la tabla, BlockingQueue agrega bloqueo en las dos operaciones principales de agregar, ver y eliminar, y puede optar por bloquear todo el tiempo o volver a un valor especial después de bloquear por un período de tiempo.

1.2 Apuntes de clase

Veamos qué información podemos obtener de la anotación de clase de LinkedBlockingQueue:

  1. Basado en una cola de bloqueo de lista vinculada, la estructura de datos subyacente es una lista vinculada;
  2. La lista vinculada mantiene la cola de primero en entrar, primero en salir, los nuevos elementos se colocan al final del equipo y los elementos adquiridos se toman del jefe del equipo;
  3. El tamaño de la lista vinculada se puede establecer durante la inicialización, y el valor predeterminado es el valor máximo de Integer;
  4. Puede utilizar todas las operaciones de las dos interfaces de Colección e Iterador, porque la interfaz de las dos está implementada.

1.3 Composición interna

La estructura interna de LinkedBlockingQueue se divide simplemente en tres partes: 链表存储 + 锁 + 迭代器veamos el código fuente.

// 链表结构 begin
//链表的元素
static class Node<E> {
    E item;
 
    //当前元素的下一个,为空表示当前节点是最后一个
    Node<E> next;
 
    Node(E x) { item = x; }
}
 
//链表的容量,默认 Integer.MAX_VALUE
private final int capacity;
 
//链表已有元素大小,使用 AtomicInteger,所以是线程安全的
private final AtomicInteger count = new AtomicInteger();
 
//链表头
transient Node<E> head;
 
//链表尾
private transient Node<E> last;
// 链表结构 end
 
// 锁 begin
//take 时的锁
private final ReentrantLock takeLock = new ReentrantLock();
 
// take 的条件队列,condition 可以简单理解为基于 ASQ 同步机制建立的条件队列
private final Condition notEmpty = takeLock.newCondition();
 
// put 时的锁,设计两把锁的目的,主要为了 take 和 put 可以同时进行
private final ReentrantLock putLock = new ReentrantLock();
 
// put 的条件队列
private final Condition notFull = putLock.newCondition();
// 锁 end
 
// 迭代器 
// 实现了自己的迭代器
private class Itr implements Iterator<E> {
………………
}

Desde el punto de vista del código, la estructura es muy clara y las tres estructuras hacen su trabajo:

  1. El papel de la lista vinculada es guardar el nodo actual. Los datos en el nodo pueden ser cualquier cosa. Es un tipo genérico. Por ejemplo, cuando la cola se aplica al grupo de hilos, el nodo es el hilo. Por ejemplo, la cola se aplica a la cola de mensajes, y el nodo es el mensaje. El significado del nodo depende principalmente de la escena donde se usa la cola;
  2. Hay bloqueos de toma y cierre para garantizar la seguridad del hilo durante las operaciones de la cola. Dos tipos de bloqueos están diseñados para que las operaciones de toma y colocación se puedan realizar simultáneamente sin afectarse entre sí.

1.4 Inicialización

Hay tres formas de inicializar:

  1. Especifique el tamaño de la lista vinculada;
  2. No especifique el tamaño de la lista vinculada, el valor predeterminado es el valor máximo de Integer;
  3. Inicializar los datos de recopilación existentes.

El código fuente es el siguiente:

// 不指定容量,默认 Integer 的最大值
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}
// 指定链表容量大小,链表头尾相等,节点值(item)都是 null
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}
 
// 已有集合数据进行初始化
public LinkedBlockingQueue(Collection<? extends E> c) {
    this(Integer.MAX_VALUE);
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); // Never contended, but necessary for visibility
    try {
        int n = 0;
        for (E e : c) {
            // 集合内的元素不能为空
            if (e == null)
                throw new NullPointerException();
            // capacity 代表链表的大小,在这里是 Integer 的最大值
            // 如果集合类的大小大于 Integer 的最大值,就会报错
            // 其实这个判断完全可以放在 for 循环外面,这样可以减少 Integer 的最大值次循环(最坏情况)
            if (n == capacity)
                throw new IllegalStateException("Queue full");
            enqueue(new Node<E>(e));
            ++n;
        }
        count.set(n);
    } finally {
        putLock.unlock();
    }
}

Para el código fuente de inicialización, explicamos dos puntos:

  1. Durante la inicialización, el tamaño de la capacidad no afectará el rendimiento, solo afectará el uso posterior, ya que la cola de inicialización es demasiado pequeña, es fácil causar el error de que la cola está llena sin informar cuánto;
  2. Al inicializar un conjunto de datos dado, el código fuente proporciona una demostración poco elegante. No nos oponemos a verificar si el tamaño de la lista vinculada actual excede la capacidad en cada bucle for, pero esperamos comenzar antes de que comience el bucle for. Haz un paso en este tipo de trabajo. Por ejemplo, el tamaño del conjunto dado es 1 w y el tamaño de la lista vinculada es 9k. Según la implementación del código actual, solo se puede encontrar cuando el bucle for es 9k. El tamaño original del conjunto dado ya es mayor que el tamaño de la lista vinculada, lo que resulta en 9k ciclos. Es un desperdicio de recursos. Es mejor verificarlo una vez antes del ciclo for. Si 1w> 9k, solo informe el error.

2 nuevo bloqueo

Hay muchos métodos nuevos, tales como: agregar, poner, ofrecer, la diferencia entre los tres se menciona anteriormente. Tomemos como ejemplo el método put. El método put se bloqueará hasta que la cola esté llena y continuará ejecutándose hasta que la cola no esté llena y cuando se despierte. El código fuente es el siguiente:

// 把e新增到队列的尾部。
// 如果有可以新增的空间的话,直接新增成功,否则当前线程陷入等待
public void put(E e) throws InterruptedException {
    // e 为空,抛出异常
    if (e == null) throw new NullPointerException();
    // 预先设置 c 为 -1,约定负数为新增失败
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    // 设置可中断锁
    putLock.lockInterruptibly();
    try {
        // 队列满了
        // 当前线程阻塞,等待其他线程的唤醒(其他线程 take 成功后就会唤醒此处被阻塞的线程)
        while (count.get() == capacity) {
            // await 无限等待
            notFull.await();
        }
 
        // 队列没有满,直接新增到队列的尾部
        enqueue(node);
 
        // 新增计数赋值,注意这里 getAndIncrement 返回的是旧值
        // 这里的 c 是比真实的 count 小 1 的
        c = count.getAndIncrement();
 
        // 如果链表现在的大小 小于链表的容量,说明队列未满
        // 可以尝试唤醒一个 put 的等待线程
        if (c + 1 < capacity)
            notFull.signal();
 
    } finally {
        // 释放锁
        putLock.unlock();
    }
    // c==0,代表队列里面有一个元素
    // 会尝试唤醒一个take的等待线程
    if (c == 0)
        signalNotEmpty();
}
// 入队,把新元素放到队尾
private void enqueue(Node<E> node) {
    last = last.next = node;
}

Del código fuente podemos resumir los siguientes puntos:

  1. Para agregar datos a la cola, el primer paso es bloquear, para que los nuevos datos sean seguros para subprocesos;
  2. Los nuevos datos en la cola se pueden agregar simplemente al final de la lista vinculada;
  3. Al agregar, si la cola está llena, el subproceso actual se bloqueará. La capa inferior de bloqueo es la capacidad de bloqueo. La implementación de la capa inferior también está relacionada con la cola. El principio del que hablaremos en el capítulo de bloqueo;
  4. Después de que los nuevos datos se agreguen correctamente, en el momento apropiado, se despertará el hilo de espera en espera (cuando la cola no está llena) o el hilo de espera en espera (cuando la cola no está vacía). Bloquee el hilo y continúe ejecutándose, asegurándose de que no se pierda el tiempo de evocación.

Lo anterior es el principio del método put. En cuanto al bloqueo del método de oferta durante más de una vez, todavía no tiene éxito y volverá directamente a la implementación del valor predeterminado. En comparación con el método put, solo se han modificado unas pocas líneas de código, como se muestra en la siguiente captura de pantalla:
Inserte la descripción de la imagen aquí

3 Eliminar bloque

Hay muchas formas de eliminar, principalmente observamos dos cuestiones clave:

  1. ¿Cuál es el principio de eliminación?
  2. Cómo darse cuenta de la diferencia entre ver y eliminar y solo ver y no eliminar.

Primero, veamos el primer problema. Tomemos el método take como un ejemplo para explicar el código fuente subyacente que se ve y se elimina :

// 阻塞拿数据
public E take() throws InterruptedException {
    E x;
    // 默认负数,代表失败
    int c = -1;
    // count 代表当前链表数据的真实大小
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        // 空队列时,阻塞,等待其他线程唤醒
        while (count.get() == 0) {
            notEmpty.await();
        }
        // 非空队列,从队列的头部拿一个出来
        x = dequeue();
        // 减一计算,注意 getAndDecrement 返回的值是旧值
        // c 比真实的 count 大1
        c = count.getAndDecrement();
        
        // 如果队列里面有值,从 take 的等待线程里面唤醒一个。
        // 意思是队列里面有值啦,唤醒之前被阻塞的线程
        if (c > 1)
            notEmpty.signal();
    } finally {
        // 释放锁
        takeLock.unlock();
    }
    // 如果队列空闲还剩下一个,尝试从 put 的等待线程中唤醒一个
    if (c == capacity)
        signalNotFull();
    return x;
}
// 队头中取数据
private E dequeue() {
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h; // help GC
    head = first;
    E x = first.item;
    first.item = null;// 头节点指向 null,删除
    return x;
}

El proceso general es muy similar al de put, que consiste en bloquear primero y luego tomar los datos del encabezado de la cola. Si la cola está vacía, se bloqueará hasta que la cola tenga valor.

Es más simple ver los elementos sin eliminarlos. Simplemente saque los datos al principio de la cola. Tomamos un vistazo como ejemplo. El código fuente es el siguiente:

// 查看并不删除元素,如果队列为空,返回 null
public E peek() {
    // count 代表队列实际大小,队列为空,直接返回 null
    if (count.get() == 0)
        return null;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        // 拿到队列头
        Node<E> first = head.next;
        // 判断队列头是否为空,并返回
        if (first == null)
            return null;
        else
            return first.item;
    } finally {
        takeLock.unlock();
    }
}

Se puede ver que la lógica de tomar datos del jefe del equipo no es consistente entre ver y eliminar, y no eliminar, lo que lleva a que uno borre y uno no borre los datos del jefe del equipo.

4 Resumen

Este artículo presenta la cola de la lista vinculada a través del código fuente de LinkedBlockingQueue. Cuando la cola está llena y vacía, ¿qué le sucede a la cola al agregar y eliminar datos?

La cola en sí es una herramienta de bloqueo. Podemos aplicar esta herramienta a varios escenarios de bloqueo. Por ejemplo, la cola se aplica al grupo de subprocesos. Cuando el grupo de subprocesos se agota, colocamos todas las solicitudes nuevas en la cola de bloqueo y esperamos; la aplicación de la cola En la cola de mensajes, cuando la capacidad de procesamiento del consumidor es limitada, podemos poner el mensaje en la cola y esperar a que el consumidor consuma lentamente; cada vez que se aplica a un nuevo escenario, es una nueva herramienta técnica, así que aprenda la cola, Útil

Publicado 40 artículos originales · ganado elogios 1 · vistas 4986

Supongo que te gusta

Origin blog.csdn.net/aha_jasper/article/details/105523782
Recomendado
Clasificación