Explicación detallada de los escenarios de aplicación de las herramientas de concurrencia JUC en los principales fabricantes.

jdk proporciona varias herramientas de sincronización que son más avanzadas que las sincronizadas, incluidas ReentrantLock, Semaphore, CountDownLatch, CyclicBarrier, etc., que pueden lograr operaciones multiproceso más ricas.

Bloqueo reentrante

ReentrantLock es un bloqueo exclusivo reentrante que permite que el mismo hilo adquiera el mismo bloqueo varias veces sin ser bloqueado.
Su función es similar a sincronizada, que es un bloqueo mutex y puede garantizar la seguridad de los subprocesos. En comparación con sincronizado, ReentrantLock tiene las siguientes características:

  • interrumpible
  • Se puede configurar el tiempo de espera
  • Se puede configurar para bloqueo justo
  • Admite múltiples variables de condición
  • Al igual que los sincronizados, ambos admiten la reentrada.

Su principal escenario de aplicación es el acceso exclusivo a recursos compartidos en un entorno de subprocesos múltiples para garantizar la coherencia y seguridad de los datos.

Insertar descripción de la imagen aquí

API de uso común

Interfaz de bloqueo

ReentrantLock implementa la especificación de la interfaz Lock y las API comunes son las siguientes:

interfaz ilustrar
bloqueo vacío() Adquirir el candado. El hilo actual que llama a este método adquirirá el candado. Cuando se adquiere el candado, el método devuelve
void lockInterruptfully() lanza InterruptedException La adquisición de bloqueo interrumpible se diferencia del método lock() en que este método responderá a las interrupciones, es decir, el hilo actual puede interrumpirse durante la adquisición del bloqueo.
tryLock booleano() Intente adquirir el bloqueo sin bloqueo y regrese inmediatamente después de llamar a este método. Devuelve verdadero si se puede obtener; de lo contrario, devuelve falso
tryLock booleano (largo tiempo, unidad TimeUnit) arroja InterruptedException Se agota el tiempo de espera para adquirir el bloqueo, el subproceso actual se devolverá en las siguientes tres situaciones: el subproceso actual adquiere el bloqueo dentro del período de tiempo de espera; el subproceso actual se interrumpe dentro del período de tiempo de espera; cuando finaliza el período de tiempo de espera, se devuelve falso
desbloqueo nulo() desbloquear bloqueo
Condición nuevaCondición() Obtenga el componente de notificación en espera, que está vinculado al bloqueo actual. El hilo actual solo puede llamar al método await() del componente después de adquirir el bloqueo. Después de la llamada, el hilo actual liberará el bloqueo.

gramática básica

//加锁  阻塞 
lock.lock(); 
try {
    
      
    ...
} finally {
    
     
    // 解锁 
    lock.unlock();  
}
//尝试加锁   非阻塞
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    
    
    try {
    
    
        ...
    } finally {
    
    
        lock.unlock();
    }
}

Hay 4 cuestiones a las que prestar atención al utilizar:

  • De forma predeterminada, ReentrantLock es un bloqueo injusto en lugar de un bloqueo justo;
  • La cantidad de veces que se aplica el bloqueo y la cantidad de veces que se libera el bloqueo deben ser consistentes; de lo contrario, provocará un bloqueo de subprocesos o una excepción en el programa;
  • La operación de bloqueo debe colocarse antes del código de prueba, para evitar la excepción de no bloquear correctamente pero luego liberar el bloqueo;
  • El abrepuertas debe colocarse finalmente, de lo contrario provocará que el hilo se bloquee.

Uso de ReentrantLock

Bloqueo exclusivo: simula la escena de captura de boletos

8 boletos, 10 personas los agarran, ¿qué problemas pasarán si no están bloqueados?

/**
 *  模拟抢票场景
 */
public class ReentrantLockDemo {
    
    

    private final ReentrantLock lock = new ReentrantLock();//默认非公平
    private static int tickets = 8; // 总票数

    public void buyTicket() {
    
    
        lock.lock(); // 获取锁
        try {
    
    
            if (tickets > 0) {
    
     // 还有票
                try {
    
    
                    Thread.sleep(10); // 休眠10ms,模拟出并发效果
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "购买了第" + tickets-- + "张票");
            } else {
    
    
                System.out.println("票已经卖完了," + Thread.currentThread().getName() + "抢票失败");
            }

        } finally {
    
    
            lock.unlock(); // 释放锁
        }
    }


    public static void main(String[] args) {
    
    
        ReentrantLockDemo ticketSystem = new ReentrantLockDemo();
        for (int i = 1; i <= 10; i++) {
    
    
            Thread thread = new Thread(() -> {
    
    

                ticketSystem.buyTicket(); // 抢票

            }, "线程" + i);
            // 启动线程
            thread.start();

        }


        try {
    
    
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
    
            throw new RuntimeException(e);
        }
        System.out.println("剩余票数:" + tickets);
    }
}

El efecto de no bloquear: hay un problema de sobreventa
Insertar descripción de la imagen aquí
El efecto de bloquear: normalmente, dos personas no pudieron conseguir boletos.
Insertar descripción de la imagen aquí

Bloqueo justo y bloqueo injusto

ReentrantLock admite dos modos: bloqueo justo y bloqueo injusto:

  • Bloqueo justo: cuando un hilo adquiere un bloqueo, lo adquiere en el orden de espera.
  • Bloqueo injusto: cuando un hilo adquiere un bloqueo, no lo adquiere en el orden de espera, sino que lo adquiere aleatoriamente. ReentrantLock tiene por defecto un bloqueo injusto
ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁  
ReentrantLock lock = new ReentrantLock(true); //公平锁  

Por ejemplo, al comprar un boleto, puede haber un escenario de salto de cola. Permitir el salto de cola es un bloqueo injusto, como se muestra a continuación:
Insertar descripción de la imagen aquí

cerradura reentrante

  • Los bloqueos reentrantes, también conocidos como bloqueos recursivos, significan que cuando el mismo hilo adquiere el bloqueo en el método externo, el método interno que ingresa al hilo adquirirá automáticamente el bloqueo (el requisito previo es que el objeto de bloqueo debe ser el mismo objeto). y no se verá afectado por el bloqueo anterior, ha sido adquirido pero aún no ha sido liberado y está bloqueado.
  • ReentrantLock y sincronizado en Java son bloqueos reentrantes. Una ventaja de los bloqueos reentrantes es que pueden evitar puntos muertos hasta cierto punto . En el desarrollo real, los bloqueos reentrantes se utilizan a menudo en escenarios como operaciones recursivas, llamadas a otros métodos en la misma clase y anidamiento de bloqueos.
class Counter {
    
    
    private final ReentrantLock lock = new ReentrantLock(); // 创建 ReentrantLock 对象

    public void recursiveCall(int num) {
    
    
        lock.lock(); // 获取锁
        try {
    
    
            if (num == 0) {
    
    
                return;
            }
            System.out.println("执行递归,num = " + num);
            recursiveCall(num - 1);
        } finally {
    
    
            lock.unlock(); // 释放锁
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Counter counter = new Counter(); // 创建计数器对象

        // 测试递归调用
        counter.recursiveCall(10);
    }
}

Insertar descripción de la imagen aquí

Combinado con Condición para implementar el modelo productor-consumidor.

  • La clase Condición se proporciona en la biblioteca de clases java.util.concurrent para lograr la coordinación entre subprocesos.
  • Llame al método Condition.await() para hacer que el hilo espere, y otros hilos llaman al método Condition.signal() o Condition.signalAll() para activar el hilo en espera.

Nota : Los métodos await() y signal() de la condición de llamada deben estar dentro de la protección de bloqueo.

Caso : Implementación de una cola simple basada en ReentrantLock y Condition

public class ReentrantLockDemo3 {
    
    

    public static void main(String[] args) {
    
    
        // 创建队列
        Queue queue = new Queue(5);
        //启动生产者线程
        new Thread(new Producer(queue)).start();
        //启动消费者线程
        new Thread(new Customer(queue)).start();

    }
}

/**
 * 队列封装类
 */
class Queue {
    
    
    private Object[] items ;
    int size = 0;
    int takeIndex;
    int putIndex;
    private ReentrantLock lock;
    public Condition notEmpty; //消费者线程阻塞唤醒条件,队列为空阻塞,生产者生产完唤醒
    public Condition notFull; //生产者线程阻塞唤醒条件,队列满了阻塞,消费者消费完唤醒

    public Queue(int capacity){
    
    
        this.items = new Object[capacity];
        lock = new ReentrantLock();
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }


    public void put(Object value) throws Exception {
    
    
        //加锁
        lock.lock();
        try {
    
    
            while (size == items.length)
                // 队列满了让生产者等待
                notFull.await();

            items[putIndex] = value;
            if (++putIndex == items.length)
                putIndex = 0;
            size++;
            notEmpty.signal(); // 生产完唤醒消费者

        } finally {
    
    
            System.out.println("producer生产:" + value);
            //解锁
            lock.unlock();
        }
    }

    public Object take() throws Exception {
    
    
        lock.lock();
        try {
    
    
            // 队列空了就让消费者等待
            while (size == 0)
                notEmpty.await();

            Object value = items[takeIndex];
            items[takeIndex] = null;
            if (++takeIndex == items.length)
                takeIndex = 0;
            size--;
            notFull.signal(); //消费完唤醒生产者生产
            return value;
        } finally {
    
    
            lock.unlock();
        }
    }
}

/**
 * 生产者
 */
class Producer implements Runnable {
    
    

    private Queue queue;

    public Producer(Queue queue) {
    
    
        this.queue = queue;
    }

    @Override
    public void run() {
    
    
        try {
    
    
            // 隔1秒轮询生产一次
            while (true) {
    
    
                Thread.sleep(1000);
                queue.put(new Random().nextInt(1000));
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

/**
 * 消费者
 */
class Customer implements Runnable {
    
    

    private Queue queue;

    public Customer(Queue queue) {
    
    
        this.queue = queue;
    }

    @Override
    public void run() {
    
    
        try {
    
    
            // 隔2秒轮询消费一次
            while (true) {
    
    
                Thread.sleep(2000);
                System.out.println("consumer消费:" + queue.take());
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

Resumen de escenarios de aplicación.

Los escenarios de aplicación específicos de ReentrantLock son los siguientes:

  • Para resolver el problema de la competencia de múltiples subprocesos por los recursos, como que varios subprocesos escriban en la misma base de datos al mismo tiempo, puede usar ReentrantLock para asegurarse de que solo un subproceso pueda escribir a la vez.
  • Implemente la ejecución secuencial de tareas de subprocesos múltiples, por ejemplo, después de que un subproceso completa una tarea, deje que otro subproceso realice la tarea.
  • Implemente un mecanismo de notificación/espera de subprocesos múltiples, por ejemplo, después de que un subproceso completa una tarea, notifique a otros subprocesos para que continúen ejecutando la tarea.

Semáforo

Semaphore (semaphore) es una herramienta de sincronización utilizada en programación multiproceso, que se utiliza para controlar la cantidad de subprocesos que acceden a un recurso al mismo tiempo.
Insertar descripción de la imagen aquí

  • Semaphore mantiene un contador y los subprocesos pueden obtener la licencia en Semaphore llamando al método adquirir().
  • Cuando el contador llega a 0, el hilo que llama a adquirir() se bloqueará hasta que otro hilo libere la licencia;
  • Un subproceso puede liberar la licencia en Semaphore llamando al método release(), que aumenta el contador en Semaphore, permitiendo que más subprocesos accedan al recurso compartido.

API de uso común

Constructor

    public Semaphore(int permits) {
    
    
        sync = new NonfairSync(permits);
    }
    public Semaphore(int permits, boolean fair) {
    
    
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }
  • permisos representa el número de licencias (número de recursos)
  • justo significa equidad. Si esto se establece en verdadero, el hilo que se ejecutará la próxima vez será el hilo que haya esperado más tiempo.

Métodos comunes

  • adquirir() significa bloquear y obtener permiso
  • El método tryAcquire () devolverá inmediatamente falso si no hay permiso y el hilo que desea obtener el permiso no se bloqueará.
  • release() significa liberar el permiso

uso del semáforo

Semaphore implementa limitación de corriente en la interfaz de servicio

@Slf4j
public class SemaphoreDemo {
    
    

    /**
     * 同一时刻最多只允许有两个并发
     */
    private static Semaphore semaphore = new Semaphore(2);

    private static Executor executor = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
    
    
        for(int i=0;i<10;i++){
    
    
            executor.execute(()->getProductInfo2());
        }
    }

    public static String getProductInfo() {
    
    
        try {
    
    
            semaphore.acquire();
            log.info("请求服务");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
    
    
            throw new RuntimeException(e);
        }finally {
    
    
            semaphore.release();
        }
        return "返回商品详情信息";
    }

    public static String getProductInfo2() {
    
    

        if(!semaphore.tryAcquire()){
    
    
            log.error("请求被流控了");
            return "请求被流控了";
        }
        try {
    
    
            log.info("请求服务");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
    
    
            throw new RuntimeException(e);
        }finally {
    
    
            semaphore.release();
        }
        return "返回商品详情信息";
    }
}

Semaphore implementa un grupo de conexiones de bases de datos

public class SemaphoreDemo2 {
    
    

    public static void main(String[] args) {
    
    
        final ConnectPool pool = new ConnectPool(2);
        ExecutorService executorService = Executors.newCachedThreadPool();

        //5个线程并发来争抢连接资源
        for (int i = 0; i < 5; i++) {
    
    
            final int id = i + 1;
            executorService.execute(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    Connect connect = null;
                    try {
    
    
                        System.out.println("线程" + id + "等待获取数据库连接");
                        connect = pool.openConnect();
                        System.out.println("线程" + id + "已拿到数据库连接:" + connect);
                        //进行数据库操作2秒...然后释放连接
                        Thread.sleep(2000);
                        System.out.println("线程" + id + "释放数据库连接:" + connect);

                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    } finally {
    
    
                        pool.releaseConnect(connect);
                    }

                }
            });
        }
    }
}

//数据库连接池
class ConnectPool {
    
    
    private int size;
    private Connect[] connects;

    //记录对应下标的Connect是否已被使用
    private boolean[] connectFlag;
    //信号量对象
    private Semaphore semaphore;

    /**
     * size:初始化连接池大小
     */
    public ConnectPool(int size) {
    
    
        this.size = size;
        semaphore = new Semaphore(size, true);
        connects = new Connect[size];
        connectFlag = new boolean[size];
        initConnects();//初始化连接池
    }

    private void initConnects() {
    
    
        for (int i = 0; i < this.size; i++) {
    
    
            connects[i] = new Connect();
        }
    }

    /**
     * 获取数据库连接
     *
     * @return
     * @throws InterruptedException
     */
    public Connect openConnect() throws InterruptedException {
    
    
        //得先获得使用许可证,如果信号量为0,则拿不到许可证,一直阻塞直到能获得
        semaphore.acquire();
        return getConnect();
    }

    private synchronized Connect getConnect() {
    
    
        for (int i = 0; i < connectFlag.length; i++) {
    
    
            if (!connectFlag[i]) {
    
    
                //标记该连接已被使用
                connectFlag[i] = true;
                return connects[i];
            }
        }
        return null;
    }

    /**
     * 释放某个数据库连接
     */
    public synchronized void releaseConnect(Connect connect) {
    
    
        for (int i = 0; i < this.size; i++) {
    
    
            if (connect == connects[i]) {
    
    
                connectFlag[i] = false;
                semaphore.release();
            }
        }
    }
}

/**
 * 数据库连接
 */
class Connect {
    
    

    private static int count = 1;
    private int id = count++;

    public Connect() {
    
    
        //假设打开一个连接很耗费资源,需要等待1秒
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("连接#" + id + "#已与数据库建立通道!");
    }

    @Override
    public String toString() {
    
    
        return "#" + id + "#";

    }
}

Insertar descripción de la imagen aquí

Resumen de escenarios de aplicación.

  • Limitación actual: Semaphore se puede utilizar para limitar la cantidad de accesos simultáneos a recursos compartidos para controlar el tráfico del sistema.
  • Grupos de recursos: Semaphore se puede utilizar para implementar grupos de recursos para mantener un conjunto limitado de recursos compartidos.

cuenta atrás

CountDownLatch es una clase auxiliar de sincronización que permite que uno o más subprocesos esperen hasta que otros subprocesos completen un conjunto de operaciones.

  • CountDownLatch se inicializa con el valor de recuento dado (recuento).
  • El método de espera bloquea el valor de conteo actual (conteo),
  • Dado que la llamada al método countDown llega a 0, todos los subprocesos en espera se liberarán después de que el conteo llegue a 0, y las llamadas posteriores al método await regresarán inmediatamente.
  • Este es un fenómeno único: el recuento no se restablecerá.

Supongo que te gusta

Origin blog.csdn.net/beautybug1126/article/details/131969997
Recomendado
Clasificación