Programación concurrente JUC (1) Thread, TreadPoolExecutor, BlockingQueue, Synchronized, Lock, clases auxiliares JUC

Hilo

Cómo crear multiproceso

Heredar hilo

  1. Método 1: heredar la clase Thread y crear los pasos:
    • Defina la subclase MyThread para heredar Thread y reescriba el método run()
    • Crea un objeto de la clase MyThread
    • Llame al método start () del objeto de subproceso para iniciar el subproceso (el método run () se ejecuta después del inicio)

Debido a que este método de creación ya heredó la clase Thread, no puede heredar otras clases, lo que no favorece la expansión.

  1. Método 2: declarar una clase que implemente la interfaz Runnable.
    • Definición Defina una clase de tarea de subproceso MyRunnable para implementar la interfaz Runnable y anular el método run().
    • Crea el objeto MyRunnable
    • Entregue el objeto MyRunnable a Thread para su procesamiento
    • Llame al método de inicio del objeto Thread para comenzar

Implementar la interfaz ejecutable.

El segundo método solo implementa la interfaz, puede continuar heredando la clase e implementando la interfaz, y la escalabilidad es más fuerte. Pero la desventaja es que hay una capa adicional de empaquetado para la programación (el objeto ejecutable debe pasarse al subproceso para construir el objeto del subproceso)
inserte la descripción de la imagen aquí
Implementación del método 1:

public class MyThread extends Thread{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 5; ++i) {
    
    
            System.out.println("子线程执行输出:" + i);
        }
    }
}
 Thread t1 = new MyThread();
 t1.start();

Implementación del método dos:

Thread t = new Thread(new Runnable() {
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            System.out.println(i);
        }
    }
});
for (int i = 0; i < 10; i++) {
    
    
    System.out.println("主线程:" + i);
}

Por supuesto, puedes utilizar expresiones lambda para taquigrafía.

 Thread t = new Thread(() -> {
    
    
     for (int i = 0; i < 10; i++) {
    
    
         System.out.println( "子线程" + i);
     }
 });
 for (int i = 0; i < 10; i++) {
    
    
     System.out.println("主线程:" + i);
 }
  1. Método 3
    Hay un problema en los dos primeros métodos de creación:
    • Ninguno de sus métodos run() reescritos puede devolver resultados directamente
    • No apto para escenarios empresariales que necesitan devolver resultados de ejecución de subprocesos.

Defina una clase que implemente la interfaz Callable y ensamblela en una FutureTask

jdk5 utiliza interfaces Callable y FutureTask para realizar las funciones anteriores.

Crear pasos:

  • Defina la clase para implementar la interfaz invocable, reescriba el método de llamada y encapsule las cosas por hacer.
  • Utilice FutureTask para encapsular el objeto invocable en un objeto de tarea de subproceso.
  • Entregue el objeto de tarea de subproceso a Thread para su procesamiento.
  • Llame al método de inicio de Thread para iniciar el hilo y ejecutar la tarea.
  • Una vez ejecutado el hilo, utilice el método get() de FutureTask para obtener el resultado de la ejecución de la tarea.

Implementación del método tres:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo3 {
    
    
    public static void main(String[] args) {
    
    
        Callable<String> call1 = new MyCallable(100000);
        FutureTask<String> f1 = new FutureTask<>(call1);
        Thread t1 = new Thread(f1);
        t1.start();

        Callable<String> call2 = new MyCallable(100000);
        FutureTask<String> f2 = new FutureTask<>(call2);
        Thread t2 = new Thread(f2);
        t2.start();

        try {
    
    
            // 如果f1没有执行完毕,那么get这里会等待,直至完成
            String r1 =  f1.get();
            System.out.println("第一个结果:" + r1);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
            // 如果f2没有执行完毕,那么get这里会等待,直至完成
        try {
    
    
            String r2 =  f2.get();
            System.out.println("第二个结果:" + r2);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

class MyCallable implements Callable<String> {
    
    
    private int n;
    public MyCallable (int n) {
    
    
        this.n = n;
    }
    @Override
    public String call() throws Exception {
    
    
        int sum = 0;
        for (int i = 0; i <= n; i++) {
    
    
            sum += i;
        }
        return "子线程执行的结果是:" + sum;
    }
}

API comunes

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

API nativa

dormir

Cuando se llama al método de suspensión de un subproceso, hace que el subproceso que se está ejecutando actualmente entre en suspensión (detenga temporalmente la ejecución) durante la cantidad especificada de milisegundos, sujeto a la precisión y exactitud de los temporizadores y programadores del sistema. Si algún subproceso interrumpe el subproceso actual, se lanzará una InterruptedException para borrar el estado de interrupción del subproceso actual.

interrumpir

  • Cuando un hilo llama a interrupción (), si el hilo está en un estado activo normal, entonces el indicador de interrupción del hilo se establecerá en verdadero, nada más, el hilo con el indicador de interrupción configurado continuará ejecutándose normalmente, sin verse afectado. Es decir, interrupción () solo establece el indicador de interrupción, pero en realidad no interrumpe el hilo y requiere la cooperación del hilo llamado. Es como decirle a una persona que se calle, pero al final si esa persona se calla o no depende de su cooperación.
  • Si el hilo está en un estado bloqueado (como dormir, esperar, unirse, etc.), llame al método de interrupción () del objeto del hilo actual en otro hilo, entonces el hilo saldrá inmediatamente del estado bloqueado y el estado de interrupción La bandera se borrará y se lanzará una excepción InterruptedException.
  • Para subprocesos inactivos, llamar a interrupción() no tiene ningún efecto.

unirse

El método de unión permite que un subproceso se ejecute antes de unirse a otro subproceso. Durante la ejecución de este subproceso, otros subprocesos entran en estado de bloqueo. Por supuesto, también puede especificar el parámetro de entrada de unión (especifique el período de tiempo de espera para la ejecución en espera) y esperar durante unos pocos milisegundos como máximo para que el hilo termine. Un tiempo de espera de 0 significa esperar para siempre.

public class demo1 {
    
    
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            try {
    
    
                TimeUnit.SECONDS.sleep(2);
                System.out.println("+++++++++++++");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        });
        t1.start();
        try {
    
    
            t1.join();
            System.out.println("ok");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

producir

El significado literal de ceder es ceder. Llamar a este método indica al programador que el subproceso actual está dispuesto a renunciar a su uso actual del procesador, que el programador puede ignorar.

El rendimiento es un intento heurístico de mejorar el progreso relativo entre subprocesos que de otro modo abusarían de la CPU. Por lo general, existen dos escenarios de uso cuando se utiliza el método de rendimiento:

  • El uso del rendimiento debe ir acompañado de análisis detallados y evaluaciones comparativas para garantizar que realmente tenga el efecto deseado, pero este enfoque rara vez se utiliza. Puede ser útil para fines de depuración o prueba, puede ayudar a reproducir errores debido a condiciones de carrera.
  • También puede resultar útil al diseñar estructuras de control de concurrencia como las del paquete java.util.concurrent.locks.
public class TestYield {
    
    
    public static void main(String[] args) {
    
    
        MyThread thread1 = new MyThread("thread-1");
        MyThread thread2 = new MyThread("thread-2");
        thread1.start();
        thread2.start();
    }
 
    private static class MyThread extends Thread {
    
    
        public MyThread(String name) {
    
    
            super(name);
        }
        @Override
        public void run() {
    
    
            for (int i = 1; i <= 5; i++) {
    
    
                if (i % 2 == 0) {
    
    
                    Thread.yield();
                    System.out.println(getName() + ":" + i);
                }
            }
        }
    }
}

TreadPoolEjecutor

El grupo de subprocesos es una tecnología que puede reutilizar subprocesos. Debido a que la sobrecarga de crear nuevos subprocesos es muy alta, el uso del grupo de subprocesos puede reutilizar subprocesos y mejorar el rendimiento del programa.
inserte la descripción de la imagen aquí

Obtener el objeto del grupo de subprocesos

Desde JDK5.0, se proporciona una interfaz que representa el grupo de subprocesos: ¿
Cómo obtiene ExecutorService el objeto del grupo de subprocesos?

  • Método 1: utilice la clase de implementación ThreadPoolExecutor de ExecutorService para crear usted mismo un objeto de grupo de subprocesos. Este método es el más flexible.

  • Método 2: utilice Executors (una clase de herramienta para grupos de subprocesos) para llamar a métodos para devolver objetos del grupo de subprocesos con diferentes características.

Grupo de subprocesos integrado de ejecutores

1. newCachedThreadPool
crea un grupo de subprocesos almacenable en caché. Si la longitud del grupo de subprocesos excede las necesidades de procesamiento, puede reciclar de manera flexible los subprocesos inactivos. Si no hay un subproceso reciclable, se creará un nuevo subproceso. Las características de este tipo de grupo de subprocesos son:
casi no hay límite para la cantidad de subprocesos de trabajo creados (de hecho, hay un límite, el número es Integer. MAX_VALUE), por lo que los subprocesos se pueden agregar al grupo de subprocesos de manera flexible. .

Si no se envía ninguna tarea al grupo de subprocesos durante un tiempo prolongado, es decir, si el subproceso de trabajo está inactivo durante un tiempo específico (1 minuto de forma predeterminada), el subproceso de trabajo finalizará automáticamente. Después de la terminación, si envía una nueva tarea, el grupo de subprocesos recreará un subproceso de trabajo. Cuando utilice CachedThreadPool, debe prestar atención a controlar la cantidad de tareas; de lo contrario, debido a una gran cantidad de subprocesos que se ejecutan al mismo tiempo, es probable que cause OOM en el sistema.

2. newFixedThreadPool
crea un grupo de subprocesos con un número específico de subprocesos de trabajo. Se crea un subproceso de trabajo cada vez que se envía una tarea y, si el número de subprocesos de trabajo alcanza el número máximo inicial de grupos de subprocesos, la tarea enviada se almacena en la cola del grupo.

FixThreadPool es un grupo de subprocesos típico y excelente, que tiene las ventajas de mejorar la eficiencia del programa y ahorrar la sobrecarga de crear subprocesos. Sin embargo, cuando el grupo de subprocesos está inactivo, es decir, cuando no hay tareas ejecutables en el grupo de subprocesos, no liberará el subproceso de trabajo y también ocupará ciertos recursos del sistema.

3. newSingleThreadExecutor
crea un ejecutor de un solo subproceso, es decir, solo crea un subproceso de trabajo único para ejecutar tareas y solo usa el único subproceso de trabajo para ejecutar tareas, lo que garantiza que todas las tareas se ejecuten en el orden especificado (FIFO, LIFO, prioridad). Si este hilo finaliza de forma anormal, otro tomará su lugar, garantizando la ejecución secuencial. La característica más importante de un único subproceso de trabajo es que se garantiza que las tareas se ejecutarán secuencialmente y no habrá varios subprocesos activos en un momento dado.
4. newScheduleThreadPool
crea un grupo de subprocesos de longitud fija y admite la temporización y la ejecución periódica de tareas, y admite la temporización y la ejecución periódica de tareas.

ThreadPoolEjecutor

inserte la descripción de la imagen aquí
¿Cuándo se crean los hilos temporales?

  • Cuando se envía una nueva tarea, los subprocesos principales están ocupados, la cola de tareas también está llena y también se pueden crear subprocesos temporales, y solo entonces se crearán subprocesos temporales.
    ¿Cuándo empiezas a rechazar tareas?
  • Tanto el subproceso principal como el subproceso temporal están ocupados y la cola de tareas también está llena y la tarea será rechazada cuando llegue una nueva tarea.

inserte la descripción de la imagen aquí

El grupo de subprocesos maneja tareas ejecutables

        ExecutorService pool = new ThreadPoolExecutor(3, 5, 6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        Runnable target = () -> {
    
    
            try {
    
    
                Thread.sleep(100000);
                System.out.println(Thread.currentThread().getName() + "输出");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        };
        // 三个核心线程
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        // 五个在等待队列
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        // 等待队列满了,新加两个临时线程
        pool.execute(target);
        pool.execute(target);
        
        // 拒绝任务,抛出异常
        pool.execute(target);

    }

El grupo de subprocesos maneja tareas invocables

inserte la descripción de la imagen aquí
ejecutar ejecuta tareas ejecutables y enviar maneja tareas invocables.

Cola de bloqueo BlockingQueue

BlockingQueue es una cola FIFO (primero en entrar, primero en salir), que resuelve el problema de cómo "transmitir" datos de manera eficiente y segura en subprocesos múltiples.
inserte la descripción de la imagen aquí
Las colas de bloqueo tienen cuatro API para agregar y eliminar elementos:

  • sin bloqueo, devuelve booleano
    • Se lanzará una excepción: agregar(), eliminar(), elemento()
    • No se lanzará ninguna excepción: oferta(), encuesta(), vistazo()
  • bloquear
    • Siempre bloqueado: put(), take()
    • Puede configurar el tiempo de espera y regresar cuando se agote el tiempo: oferta (e, tiempo de espera, unidad), encuesta (tiempo de espera, unidad)

inserte la descripción de la imagen aquí

LinkedBlockingQueue cola de bloqueo de lista vinculada

Cola de bloqueo de lista doblemente enlazada.

Para FixThreadPool y SingleThreadExector, la cola de bloqueo que utilizan es LinkedBlockingQueue con una capacidad de Integer.MAX_VALUE, que puede considerarse como una cola ilimitada.

Dado que
la cantidad de subprocesos en el grupo de subprocesos de FixThreadPool es fija, no hay forma de agregar una cantidad particularmente grande de subprocesos para procesar tareas. En este momento, se necesita una cola de bloqueo sin límite de capacidad, como LinkedBlockingQueue, para almacenar tareas.

Cabe señalar aquí que, dado que la cola de tareas del grupo de subprocesos nunca estará llena, el grupo de subprocesos solo creará subprocesos con la cantidad de subprocesos principales, por lo que la cantidad máxima de subprocesos en este momento no tiene sentido para el grupo de subprocesos, porque no activará la generación de múltiples subprocesos, subprocesos según el número de subprocesos principales.

Cola síncrona SynchronousQueue

La cola síncrona SynchronousQueue no almacena elementos, siempre que se coloque un elemento en ella, es necesario sacar y sacar un elemento.

El grupo de subprocesos correspondiente es CachedThreadPool. El número máximo de subprocesos en el grupo de subprocesos CachedThreadPool es el valor máximo de Integer. Se puede entender que el número de subprocesos se puede expandir infinitamente.
CachedThreadPool es todo lo contrario del grupo de subprocesos anterior, FixedThreadPool. En el caso de FixThreadPool, la capacidad de la cola de bloqueo es ilimitada. Aquí, el número de subprocesos en CachedThreadPool se puede expandir infinitamente, por lo que el grupo de subprocesos CachedThreadPool no necesita un cola de tareas para almacenar tareas, porque una vez enviada una tarea, se reenvía directamente al hilo o crea un nuevo hilo para ejecutar sin guardarlas por separado.

SynchronousQueue<String> bq = new SynchronousQueue();
new Thread(() -> {
    
    
    try {
    
    
        bq.put("1");
        System.out.println(Thread.currentThread().getName());
        bq.put("1");
        System.out.println(Thread.currentThread().getName());
        bq.put("1");
        System.out.println(Thread.currentThread().getName());
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
}, "A").start();

new Thread(() -> {
    
    
    try {
    
    
        TimeUnit.SECONDS.sleep(2);
        bq.take();
        System.out.println(Thread.currentThread().getName());
        bq.take();
        System.out.println(Thread.currentThread().getName());
        bq.take();
        System.out.println(Thread.currentThread().getName());
    } catch (Exception e){
    
    
        e.printStackTrace();
    }
}, "B").start();
}

DelayedWorkQueue cola de bloqueo retrasada

La tercera cola de bloqueo es DelayedWorkQueue. Sus grupos de subprocesos correspondientes son ScheduledThreadPool y SingleThreadScheduledExecutor. La característica más importante de estos dos grupos de subprocesos es que pueden retrasar la ejecución de tareas, como ejecutar tareas después de un cierto período de tiempo o ejecutar tareas a intervalos regulares. . .

La característica de DelayedWorkQueue es que los elementos internos no se clasifican según el momento en que se colocan, sino que las tareas se clasifican según la duración del retraso, y el uso interno es una estructura de datos de "montón". La razón por la que el grupo de subprocesos ScheduledThreadPool y SingleThreadScheduledExecutor eligen DelayedWorkQueue es porque ellos mismos ejecutan tareas en función del tiempo, y la cola de retraso puede simplemente ordenar las tareas por tiempo para facilitar su ejecución.

sincronizado

1 、并发就是多线程操作同一个资源。

2 、在Java中,线程就是一个单独的资源类,没有任何附属操作。资源中包含并发操作的属性、方法。

Caso de concurrencia: un ejemplo de compra de boletos multiproceso:
en mi ejemplo, la clase de recurso público es Ticket, la izquierda es el número restante de boletos, cnt es el número registrado de boletos vendidos y el método sale() imprime quién lo compró. cuando hay boletos sobrantes Un boleto y muestra los boletos restantes actuales y el total de boletos vendidos.

class Ticket {
    
    
    int left;
    int cnt = 0;
    public int getLeft() {
    
    
        return left;
    }

    public void setLeft(int left) {
    
    
        this.left = left;
    }

    public Ticket(int n) {
    
    
        this.left = n;
    }
    public void sale() {
    
    
        try {
    
    
            Thread.sleep(100); // 模拟卖票耗时
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        if (left > 0) {
    
    
            ++cnt;
            --left;
            System.out.println("线程:" + Thread.currentThread().getName() + "剩余:" + left + ",共卖出:" + cnt);
        }
    }
}

En la función principal, establezca la cantidad de votos, la cantidad de personas que compran boletos y la cantidad de intentos de cada persona para comprar boletos:

public static void main(String[] args) {
    
    
    int ticketNum = 8, people = 4, chance = 5;
    Ticket t = new Ticket(ticketNum);
    System.out.println("开售前:---------" + t.getLeft());
    for (int i = 0; i < people; ++i) {
    
    
        new Thread(() -> {
    
    
            for (int j = 0; j < chance; ++j) {
    
    
                t.sale();
            }
        }, Integer.toString(i)).start();
    }
    try {
    
    
        Thread.sleep(3000);
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
}

resultado:
inserte la descripción de la imagen aquí

solución sincronizada

La base de la implementación sincronizada de la sincronización: cada objeto en Java se puede utilizar como bloqueo. En concreto, adopta las tres formas siguientes:
1. 对于普通同步方法,锁是当前实例对象。
2. 对于静态同步方法,锁是当前类的Class对象。
3. 对于同步方法块,锁是Synchonized括号里配置的对象。

Deje que sale () se convierta directamente en un método sincrónico, es decir, cuando cada hilo accede al método sale (), bloqueará el objeto de la instancia (es decir, el recurso público) donde se encuentra el método actual, luego solo un hilo Puede operar a la vez y otros subprocesos deben esperar:
inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

cerradura de bloqueo

La cerradura utiliza tres pasos:

  1. Crea un candado: Lock lk = new ReentrantLock();
  2. Obtener un candado: lk.lock
  3. try-catch-finalmente, donde la lógica empresarial que requiere control de concurrencia se escribe en try y el bloqueo se libera en finalmente para garantizar que el bloqueo se libere normalmente cuando ocurre una excepción lk.lock().
class Ticket2 {
    
    
    int left;
    int cnt = 0;
    Lock lk = new ReentrantLock();
    public int getLeft() {
    
    
        return left;
    }

    public void setLeft(int left) {
    
    
        this.left = left;
    }

    public Ticket2(int left) {
    
    
        this.left = left;
    }

    public void sale() {
    
    
        lk.lock();
        try {
    
    
            if (left > 0) {
    
    
                ++cnt;
                --left;
                System.out.println("线程:" + Thread.currentThread().getName() + "剩余:" + left + ",共卖出:" + cnt);
            }
        }
        catch (Exception e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            lk.unlock();
        }
    }
}

ReentrantLock también puede garantizar la seguridad de la concurrencia.
inserte la descripción de la imagen aquí

La diferencia entre bloqueo sincronizado y bloqueo de bloqueo

  1. Synchronized es una palabra clave incorporada de Java y Lock es una clase de Java.
  2. Sincronizado no puede adquirir el estado del bloqueo y Lock puede determinar si el bloqueo se ha adquirido.
  3. Sincronizado liberará automáticamente el bloqueo y el bloqueo debe bloquearse manualmente. Si el bloqueo no se libera, se producirá un punto muerto.
  4. Sincronizado, el hilo que no ha adquirido el bloqueo esperará para siempre Bloqueo de bloqueo, existe un mecanismo para intentar adquirir el bloqueo, y no necesariamente espera para siempre.
  5. sincronizado es reentrante e injusto; el bloqueo de bloqueo es reentrante y se pueden establecer bloqueos justos, es decir, uno son palabras clave integradas que no se pueden modificar y el otro está personalizado.
  6. Sincronizado es adecuado para bloquear una pequeña cantidad de problemas de sincronización de código, y Lock es adecuado para bloquear una gran cantidad de códigos de sincronización.

problema productor consumidor

implementación sincronizada

Los subprocesos A y B operan la misma variable num, A permite num + 1,
B permite num - 1 y los dos se usan alternativamente.

Aquí, cuando A termina, se debe notificar a B, y se debe notificar a B cuando finaliza la operación, para realizar la sincronización de subprocesos, lo que equivale a entregárselo a B para su consumo después de que se produzca A y luego notificar A después de que se completa el consumo de B.

Complete esta trilogía de programación de modelos de producción-consumo:

  • Esperar: cuando no se cumple la condición, el ciclo while espera
  • Negocio: Cuando se cumplan las condiciones, ejecutar el negocio.
  • Notificación: una vez completado el negocio, notifique a otros hilos.

Cree una clase de recurso Datos, que tenga una variable miembro num, cree dos métodos de sincronización, uno ejecuta +1 y el otro ejecuta -1. En el método principal, inicie dos subprocesos A y B e intente operar +1 y - 1 respectivamente:

class Data {
    
    
    private int num = 0;

    public synchronized void increase() {
    
    
        while (num != 0) {
    
    
            try {
    
    
                this.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        ++num;
        System.out.println(Thread.currentThread().getName() +":->"+ num);
        this.notifyAll();
    }

    public synchronized void decrease() {
    
    
        while (num != 1) {
    
    
            try {
    
    
                this.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        --num;
        System.out.println(Thread.currentThread().getName() +":->"+ num);
        this.notifyAll();
    }
}

    public static void main(String[] args) {
    
    
        Data d = new Data();
        new Thread(() -> {
    
    
            for (int i = 0; i < 5; i++) {
    
    
                d.increase();
            }
        }, "A").start();

        new Thread(() -> {
    
    
            for (int i = 0; i < 5; i++) {
    
    
                d.decrease();
            }
        }, "B").start();
    }
}

implementación de la versión de bloqueo - condición

inserte la descripción de la imagen aquí
Puede usar lock para construir variables de condición. condition proporciona el método await() y los métodos signal() y signalAll(), similares a wait(), notify() y notifyAll;

Pasos clave:

  • 1. Cree ReentrantLock() lk y obtenga la condición de lk
  • 2. Bloqueo: lk.lock()
  • 3 、 intentar-atrapar-final
    • Escriba la lógica empresarial en el intento:
      • Espere: cuando no se cumple la condición, el ciclo while espera: condition.await();
      • Negocio: Cuando se cumplan las condiciones, ejecutar el negocio.
      • Notificación: una vez completado el negocio, notifique a otros hilos: condition.signalAll();
    • Liberar el bloqueo en final: lk.unlock();
class Data2 {
    
    
    private int num = 0;
    Lock lk = new ReentrantLock();
    Condition condition = lk.newCondition();
    
    public  void increase() {
    
    
        lk.lock();
        try {
    
    
            while (num != 0) condition.await();
            ++num;
            System.out.println(Thread.currentThread().getName() +":->"+ num);
            condition.signalAll();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lk.unlock();
        }
    }

    public  void decrease() {
    
    
        lk.lock();
        try {
    
    
            while (num != 1) condition.await();
            --num;
            System.out.println(Thread.currentThread().getName() +":->"+ num);
            condition.signalAll();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lk.unlock();
        }
    }
}

la condición realiza una notificación precisa para despertarse

inserte la descripción de la imagen aquí

invocable

Se utiliza para solicitudes asincrónicas con valores de retorno para obtener resultados.
El primer paso es crear su propio objeto invocable e implementar la interfaz invocable, que necesita un parámetro genérico para identificar el tipo de resultado esperado.

class  MyCall implements Callable<Integer> {
    
    
    int a, b;

    public MyCall(int a, int b) {
    
    
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer call() throws Exception {
    
    
        TimeUnit.SECONDS.sleep(2);
        return a + b;
    }
}

El objeto Callable está empaquetado con FutureTask, es decir, está empaquetado como una tarea que se ejecutará en el futuro.

MyCall call = new MyCall(3, 4);
FutureTask future = new FutureTask(call);

inserte la descripción de la imagen aquí
La interfaz compuesta RunnableFuture está implementada en FutureTask, es decir, tiene la implementación de Runnable, por lo que se puede poner en Thead para comenzar:

        new Thread(future).start();
        Integer a = 0;
        try {
    
    
            a = (Integer) future.get();
        } catch (InterruptedException | ExecutionException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(a.toString());

¡Este futuro.get() se bloqueará!

Clases auxiliares comúnmente utilizadas por JUC

CountDownLatch (temporizador de cuenta regresiva)

CountDownLatch permite contar los subprocesos para bloquearlos en un solo lugar hasta que se ejecuten las tareas de todos los subprocesos.

Simule una escena, hay 6 estudiantes en el salón de clases y la puerta solo se puede cerrar después de que todos los estudiantes se hayan ido.

public static void main(String[] args) {
    
    
    // 1、统计num个线程的倒计时器
    int num = 6;
    CountDownLatch cn = new CountDownLatch(num);
    for (int i = 0; i < num; ++i) {
    
    
        new Thread(()->{
    
    
            try {
    
    
                Thread.sleep(100);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "离开");
            // 2、线程结束前,倒数一下
            cn.countDown();
        }, String.valueOf(i)).start();
    }
    try {
    
    
        // 3、等待所有线程结束
        cn.await();
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
    System.out.println("关门!");
}

inserte la descripción de la imagen aquí

CyclicBarrier (valla cíclica)

CyclicBarrier es muy similar a CountDownLatch, también puede implementar espera técnica entre subprocesos, lo que tiene que hacer es bloquear un grupo de subprocesos cuando llegan a una barrera (también llamado punto de sincronización) hasta que el último subproceso llegue a la barrera, solo entonces Se abrirá la puerta y todos los hilos bloqueados por la barrera seguirán funcionando.

El escenario actual se invierte, suponiendo que el número de personas que llegan antes de que el maestro alcance el número especificado, se permite que la puerta se abra:

Paso 1: cree un CyclicBarrier, especifique la cantidad de subprocesos que se cumplirán y el objeto ejecutable que se ejecutará después de que lleguen todos los subprocesos.

CyclicBarrier cb = new CyclicBarrier(num, () -> {
    
    
   System.out.println("开门!");
});

Paso 2: Antes del final de la ejecución de cada subproceso, utilice cb.await();para esperar a que se sincronicen otros subprocesos.

public static void main(String[] args) {
    
    
    // 1、等待的人数到达num后,才开门!
    int num = 6;
    CyclicBarrier cb = new CyclicBarrier(num, () -> {
    
    
        System.out.println("开门!");
    });
    for (int i = 0; i < num; ++i) {
    
    
        new Thread(()->{
    
    
            System.out.println(Thread.currentThread().getName() + "到达");
            try {
    
    
                // 2、线程结束前,需等待其他线程同步
                cb.await();
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }, String.valueOf(i)).start();
    }
}

inserte la descripción de la imagen aquí

Semáforo semáforo: permite que varios subprocesos accedan a él al mismo tiempo

Tanto sincronizado como ReentrantLock permiten que solo un subproceso acceda a un recurso a la vez.Semaphore(信号量)可以指定多个线程同时访问某个资源。

Semaphore tiene dos modos: modo justo y modo injusto.

  • Modo justo: el orden en que se llama a adquirir es el orden en que se adquieren las licencias, siguiendo FIFO
  • Modo injusto: preventivo

Método de construcción:

public Semaphore(int permits) {
    
    
        sync = new NonfairSync(permits);
    }
public Semaphore(int permits, boolean fair) {
    
    
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

Ambos métodos de construcción deben proporcionar el número de permisos. El segundo método de construcción puede especificar si es un modo justo o injusto, y el predeterminado es el modo injusto.

inserte la descripción de la imagen aquí

El escenario más utilizado es cuando los recursos son limitados y solo un número específico de subprocesos pueden acceder a un determinado recurso al mismo tiempo, como en la simulación de una escena en la que se ocupa un espacio de estacionamiento:

Paso 1:
Simular la situación de los recursos: num espacios de estacionamiento, y el usuario tiene 10
int num = 3, total = 6;
Semaphore semaphore = new Semaphore(num);

Paso 2:
try -catch -final:
try: semaphore.acquire(); // Adquirir el recurso
finalmente: semaphore.release(); // Liberar el recurso

public static void main(String[] args) {
    
    
    // 1、num个车位,而用户有total 个
    int num = 3, total = 6;
    Semaphore sm = new Semaphore(num);
    for (int i = 0; i < total; ++i) {
    
    
        new Thread(()->{
    
    
            try {
    
    
                sm.acquire();
                System.out.println(Thread.currentThread().getName() + "抢到车位");
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + "离开车位");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                sm.release();
            }
        }, String.valueOf(i)).start();
    }
}

Sólo 3 usuarios podrán ocupar la plaza de aparcamiento al mismo tiempo.
inserte la descripción de la imagen aquí

ReadWriteLock bloqueo de lectura y escritura

Utilice el bloqueo de lectura y escritura para implementar un caché personalizado y solo se permite una operación al escribir:

Paso 1: Definir un bloqueo de lectura y escritura:
readWriteLock privado readWriteLock = new ReentrantReadWriteLock();
Paso 2: Definir una función de operación de lectura:
agregar un bloqueo de lectura antes de leer
readWriteLock.readLock().lock();
Liberar el bloqueo después de leer
Paso 3: Defina la función de operación de escritura ()
para agregar un bloqueo de escritura al escribir y liberar el bloqueo después de escribir.

class MyCache {
    
    
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Map<String, Object> mp = new HashMap<>();

    public void put(String s, Object o) {
    
    
        readWriteLock.writeLock().lock();
        try {
    
    
            mp.put(s, o);
            System.out.println(Thread.currentThread().getName() + "插入:" + s);
        } catch (Exception exception) {
    
    
            exception.printStackTrace();
        } finally {
    
    
            readWriteLock.writeLock().unlock();
        }
    }

    public Object get(String s) {
    
    
        Object ans = null;
        readWriteLock.readLock().lock();
        try {
    
    
            ans = mp.get(s);
            System.out.println(Thread.currentThread().getName() + "查询:" + s);
        } catch (Exception exception) {
    
    
            exception.printStackTrace();
        } finally {
    
    
            readWriteLock.readLock().unlock();
        }
        return ans;
    }
}

Cuatro interfaces de funciones principales

Una interfaz funcional es una interfaz que define solo un método abstracto o una interfaz anotada con @FunctionalInterface. Puede haber métodos predeterminados.
Las cuatro interfaces funcionales son:

  • Interfaz de función funcional (Función): una entrada de interfaz es la entrada de la función y la otra es la salida de la función
  • Interfaz de función de consumidor (Consumidor): la entrada de la interfaz es la entrada de la función y el retorno de la función es un valor booleano
  • Interfaz funcional de suministro (proveedor)
  • Interfaz funcional de predicado (predicado)

función / funcional

Interfaz funcional: una entrada de interfaz es la entrada de la función y la otra es la salida de la función

@FunctionalInterface
public interface Function<T, R> {
    
    

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
    // ... 两个默认函数和一个静态函数
}

tipo de aserción

Interfaz de determinación: la entrada de la interfaz es la función de entrada de la función y el retorno de la función es un valor booleano

public interface Predicate<T> {
    
    

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
    // ...三个默认函数和一个静态函数
}

Tipo de consumo

Interfaz del consumidor: solo entrada, sin salida

@FunctionalInterface
public interface Consumer<T> {
    
    

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
    // ...一个默认方法
}

Tipo de alimentación

Interfaz de suministro: solo salida, sin entrada

@FunctionalInterface
public interface Supplier<T> {
    
    

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}
        Consumer consumer = (str) -> {
    
    
            System.out.println(str);
        };
        consumer.accept("Happy");
    }

Caso de uso: programación de streaming

    /**
    有5个用户,筛选
     1、ID 必须是偶数
     2、年龄必须大于23
     3、用户名转大写字母
     4、倒序排序
     5、只需要一个用户
     **/
    public static void main(String[] args) {
    
    
        List<User> list = new ArrayList<>();
        Collections.addAll(list,
                new User(0, 22, "lzy"),
                new User(1, 20, "blzy"),
                new User(2, 25, "azy"),
                new User(3, 24, "czy"),
                new User(4, 24, "dzy"),
                new User(5, 24, "ezy"),
                new User(6, 24, "fzy"),
                new User(7, 24, "gsy"));
        list.stream().filter(e -> {
    
    return e.getId() % 2 == 1;})
                     .filter(e -> {
    
    return e.getAge() > 23;})
                     .map(e -> {
    
    return e.getUsername().toUpperCase();})
                     .sorted(Comparator.reverseOrder())
                     .limit(1)
                     .forEach(System.out::println);

    }

Supongo que te gusta

Origin blog.csdn.net/baiduwaimai/article/details/132083149
Recomendado
Clasificación