Java avanzado: ventajas y desventajas de la programación concurrente

Tabla de contenido

Pros y contras de la programación concurrente

Por qué utilizar la programación concurrente (ventajas):

Desventajas de la programación concurrente:

cambio de contexto frecuente

seguridad del hilo

conceptos confusos

bloqueo y no bloqueo

modelo de bloqueo

modelo sin bloqueo

sincrónico y asincrónico

llamada sincrónica

llamada asincrónica

sección crítica

Concurrencia y Paralelismo

cambio de contexto


Pros y contras de la programación concurrente

La programación concurrente se refiere a la ejecución simultánea de múltiples tareas u operaciones independientes en un programa para mejorar el rendimiento y la capacidad de respuesta del programa.

Por qué utilizar la programación concurrente (ventajas):

  1. Mejore el rendimiento del sistema: mediante la programación concurrente, la potencia informática de los procesadores multinúcleo se puede utilizar por completo para lograr la ejecución paralela de tareas y mejorar el rendimiento del sistema y la velocidad de respuesta.
  2. Mejore la eficiencia del código: el uso de tecnología de programación concurrente puede ejecutar tareas de forma asincrónica, reducir el tiempo de espera, mejorar la eficiencia de ejecución del código y mejorar el rendimiento general del sistema.
  3. Mejore la escalabilidad del programa: a través de la programación concurrente, las tareas se pueden dividir en múltiples tareas pequeñas y procesarse en paralelo, lo cual es conveniente para agregar, ajustar o eliminar tareas de manera flexible, a fin de lograr la fácil escalabilidad y el diseño modular del sistema.
  4. Mejore la experiencia del usuario: la programación concurrente puede hacer que el programa tenga mayor capacidad de respuesta, evitar la congelación o bloqueo de la interfaz y mejorar la experiencia de interacción del usuario.

Desventajas de la programación concurrente:

  1. Alta complejidad de la programación multiproceso: la programación concurrente debe considerar cuestiones como la seguridad de los subprocesos, las condiciones de carrera y los puntos muertos, tiene altos requisitos para los desarrolladores y es más difícil escribir y depurar código concurrente.
  2. Es probable que se produzcan errores: debido a las condiciones de competencia entre subprocesos y el acceso a recursos compartidos en la programación concurrente, los códigos concurrentes incorrectos pueden provocar fácilmente inconsistencia de datos, interbloqueos y otros problemas, lo que aumenta el riesgo de errores en el programa.
  3. Pérdida de rendimiento: la creación y el cambio de contexto de subprocesos generará una cierta sobrecarga. Si la programación concurrente utiliza o administra incorrectamente demasiados subprocesos, consumirá recursos del sistema y afectará el rendimiento.

cambio de contexto frecuente

El cambio de contexto significa que cuando el sistema operativo está ejecutando una determinada tarea, necesita cambiar a otra tarea por algún motivo, guardar el contexto de la tarea actual (es decir, información de estado) y cargar el contexto de otra tarea, y luego comenzar. ejecutando la tarea. Este proceso genera una cierta sobrecarga de rendimiento porque es necesario guardar y cargar información de contexto.

En la programación de subprocesos múltiples, el cambio de contexto frecuente puede provocar una degradación del rendimiento, porque cambiar entre subprocesos requiere guardar y cargar información de contexto del subproceso, lo que requiere mucho tiempo. Por lo tanto, debemos tomar algunas medidas para reducir la cantidad de cambios de contexto a fin de aprovechar al máximo las ventajas de la programación multiproceso.

  1. Programación concurrente sin bloqueos: utilizando la idea de segmentación de bloqueos, diferentes subprocesos procesan diferentes segmentos de datos, lo que reduce el tiempo de cambio de contexto en el caso de competencia de múltiples subprocesos.
  2. Algoritmo CAS: utilice operaciones atómicas (CAS) para actualizar datos, utilice bloqueos optimistas y reduzca el cambio de contexto causado por una competencia de bloqueos innecesaria.
  3. Utilice la menor cantidad de subprocesos: evite crear subprocesos innecesarios, como evitar crear demasiados subprocesos cuando hay pocas tareas, para reducir una gran cantidad de subprocesos en estado de espera.
  4. Corrutina: implemente la programación de múltiples tareas en un solo subproceso y realice el cambio de tareas en un solo subproceso.

Nota: El cambio de contexto también es una operación que requiere relativamente tiempo. Un experimento en el libro "El arte de la programación concurrente en Java" muestra que la acumulación concurrente no es necesariamente más rápida que la acumulación en serie. Puede utilizar herramientas como Lmbench3 para medir la duración de los cambios de contexto y utilizar vmstat para medir la cantidad de cambios de contexto.

El siguiente es un ejemplo de código Java simple que demuestra cómo usar la segmentación de bloqueo para reducir la cantidad de cambios de contexto:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Main {
    private static final int SEGMENT_COUNT = 16; // 锁分段数量
    private final Lock[] segmentLocks; // 锁分段数组
    private final int[] data; // 数据数组

    public Main() {
        // 初始化锁分段数组和数据数组
        segmentLocks = new ReentrantLock[SEGMENT_COUNT];
        for (int i = 0; i < SEGMENT_COUNT; i++) {
            segmentLocks[i] = new ReentrantLock();
        }
        data = new int[SEGMENT_COUNT];
    }

    private Lock getSegmentLock(int index) {
        // 根据索引计算锁在锁分段数组中的位置
        int segmentIndex = Math.abs(index % SEGMENT_COUNT);
        return segmentLocks[segmentIndex];
    }

    /**
     * 更新数据
     *
     * @param index 索引
     * @param value 值
     */
    public void updateData(int index, int value) {
        Lock lock = getSegmentLock(index);
        lock.lock(); // 获取锁
        try {
            data[index] = value; // 更新数据
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    /**
     * 获取数据
     *
     * @param index 索引
     * @return 数据值
     */
    public int getData(int index) {
        Lock lock = getSegmentLock(index);
        lock.lock(); // 获取锁
        try {
            return data[index]; // 返回数据
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public static void main(String[] args) {
        Main main = new Main();

        // 示例调用 updateData() 和 getData() 方法
        main.updateData(0, 1);
        int value = main.getData(0);
        System.out.println("价值: " + value);//价值: 1
    }
}

seguridad del hilo

Lo más difícil de comprender en la programación multiproceso es el problema de seguridad de los subprocesos de la sección crítica. Si no le presta atención, se producirá un punto muerto. Una vez que se produzca el punto muerto, la función del sistema no estará disponible.

Para evitar la situación de punto muerto, se pueden tomar las siguientes medidas:

  1. Evite que un hilo adquiera múltiples bloqueos al mismo tiempo, evitando el riesgo de punto muerto.
  2. Cada candado solo ocupa un recurso, lo que evita que un hilo ocupe varios recursos dentro del candado.
  3. Utilice un bloqueo cronometrado, utilice el método tryLock() para intentar adquirir el bloqueo y libere el bloqueo después de un tiempo de espera para evitar que los subprocesos esperen indefinidamente.
  4. Para los bloqueos de bases de datos, asegúrese de que las operaciones de bloqueo y desbloqueo se realicen dentro de la misma conexión de base de datos para evitar fallas de desbloqueo.

Además, es importante comprender las cuestiones de atomicidad, ordenamiento y visibilidad del modelo de memoria JVM. Por ejemplo, la lectura sucia de datos significa que un subproceso modifica el valor de una variable compartida, pero no se ha vaciado a la memoria principal y otro subproceso aún ve el valor anterior al leer la variable. DCL (Double Check Lock) es una técnica de optimización que se utiliza para reducir la sobrecarga de los bloqueos, pero puede provocar carreras de datos y problemas de seguridad de subprocesos.

Aprender tecnología de programación multiproceso requiere una comprensión profunda de los conceptos y principios de la programación concurrente, dominar varias herramientas y tecnologías concurrentes y poder elegir soluciones adecuadas de acuerdo con escenarios específicos. A través del aprendizaje y la práctica, puede mejorar su capacidad de programación concurrente y el rendimiento del programa y, al mismo tiempo, mejorar su comprensión y dominio de la programación multiproceso.

Cuando se trata de problemas de seguridad de subprocesos de secciones críticas, aquí hay un ejemplo de código Java que utiliza un mecanismo de bloqueo para garantizar la seguridad de los subprocesos: 

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Main {
    private int count = 0;
    private Lock lock = new ReentrantLock(); // 创建一个可重入锁

    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++; // 临界区,对共享变量进行操作
        } finally {
            lock.unlock(); // 释放锁,确保在发生异常时也能正常释放锁
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        Main example = new Main();

        // 创建多个线程并执行increment方法
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    example.increment();
                }
            });
            thread.start();
        }

        // 等待所有线程执行完毕
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出计数结果
        System.out.println("计数= " + example.getCount());//运行结果:计数= 5000
    }
}

conceptos confusos

bloqueo y no bloqueo

El bloqueo y el no bloqueo son conceptos importantes en la programación concurrente. Describen el comportamiento de un subproceso mientras espera que se complete una operación.

modelo de bloqueo

En el modelo de bloqueo, cuando un subproceso llama a una operación, si la operación no se ha completado, el subproceso se suspenderá y entrará en estado de espera hasta que se complete la operación. Durante este proceso, el hilo no realizará otras tareas, por lo que el modelo de bloqueo es una forma de estar ocupado esperando. En Java, algunas operaciones de E/S (como leer un archivo o una conexión de red) están bloqueadas y, si la operación aún no se ha completado, el subproceso se suspenderá hasta que se complete la operación.

modelo sin bloqueo

El modelo sin bloqueo no suspende subprocesos. En el modelo sin bloqueo, después de que un subproceso invoca una operación, se devuelve un resultado inmediatamente, independientemente de si la operación se completó o no. El resultado puede ser una finalización parcial de la operación o un estado de error. Durante este proceso, el hilo puede continuar realizando otras tareas, por lo que el modelo sin bloqueo puede mejorar la concurrencia y eficiencia del sistema. En Java, algunas clases de herramientas de programación concurrentes, como Lock y Semaphore en java.util.concurrent, no son bloqueantes y proporcionan operaciones de conteo y sincronización de subprocesos sin bloqueo.

En la práctica, tanto los modelos con bloqueo como los sin bloqueo tienen ventajas y desventajas.

  1. El modelo de bloqueo es simple y fácil de entender, pero puede generar ineficiencias en el sistema.
  2. El modelo sin bloqueo puede mejorar la concurrencia y la eficiencia del sistema, pero puede requerir una lógica más compleja para procesar los resultados y el estado de las operaciones.

sincrónico y asincrónico

En programación informática, sincrónica y asincrónica son las dos formas principales de abordar las tareas y el flujo de datos.

llamada sincrónica

Una llamada sincrónica es una llamada de bloqueo, lo que significa que la persona que llama esperará a que la persona que llama complete la operación y devuelva el resultado. Mientras la persona que llama espera, su hilo se bloquea y no puede realizar otras tareas. Solo cuando la persona que llama completa la tarea y devuelve el resultado, la persona que llama continuará ejecutando el código posterior. En el modelo sincrónico, existe una relación clara de solicitud-respuesta entre la persona que llama y el destinatario, y la persona que llama debe esperar la respuesta antes de continuar con el siguiente paso.

llamada asincrónica

Una llamada asincrónica es una llamada sin bloqueo, lo que significa que la persona que llama regresa inmediatamente después de enviar la solicitud sin esperar a que la persona que llama complete la operación. La persona que llama puede continuar realizando otras tareas sin esperar a que la complete la persona que llama. En el modelo asincrónico, no existe una relación clara de solicitud-respuesta entre la persona que llama y la persona que llama, pero los resultados se entregan a través de funciones de devolución de llamada o notificaciones de eventos.

Generalmente hay dos formas de obtener el resultado de una llamada asincrónica:

  1. Sondear activamente el resultado de la llamada asincrónica;
  2. La persona que llama notifica a la persona que llama sobre el resultado de la llamada mediante devolución de llamada;

La ventaja de las llamadas asincrónicas es que pueden aumentar la concurrencia y la eficiencia del sistema, porque la persona que llama puede realizar otras tareas mientras espera el resultado. Sin embargo, el modelo asincrónico también tiene algunas desventajas, como que puede requerir una lógica más compleja para manejar funciones de devolución de llamada y notificaciones de resultados, y puede enfrentar algunos problemas de concurrencia, como condiciones de carrera y puntos muertos.

En aplicaciones prácticas, la elección de síncrono o asíncrono depende de las necesidades y escenarios específicos. Para operaciones que requieren resultados inmediatos, generalmente se usan llamadas sincrónicas; para escenarios que no requieren resultados inmediatos, como operaciones de E/S a largo plazo o solicitudes de red, generalmente se usan llamadas asincrónicas para mejorar la eficiencia y el rendimiento del sistema.

sección crítica

Una sección crítica es un concepto de programación utilizado para gestionar el acceso a recursos compartidos. En la programación multiproceso, varios subprocesos pueden intentar acceder y modificar datos compartidos al mismo tiempo, lo que puede provocar inconsistencias en los datos y otros problemas de simultaneidad. Las secciones críticas evitan estos problemas al proporcionar una forma de garantizar que solo un subproceso pueda acceder a un recurso compartido en un momento dado.

Una sección crítica suele ser un área definida en un fragmento de código que contiene operaciones que requieren acceso a recursos compartidos. Antes de ingresar a una sección crítica, un subproceso debe adquirir un bloqueo para evitar que otros subprocesos ingresen a la sección crítica al mismo tiempo. Cuando un hilo sale de la sección crítica, debe liberar el bloqueo, permitiendo que otros hilos entren en la sección crítica.

El uso de secciones críticas puede garantizar la coherencia de los datos y el control de la concurrencia. También previene las carreras de datos y otros problemas de concurrencia, como interbloqueos y falta de recursos. Sin embargo, el uso de secciones críticas también puede provocar bloqueos y esperas entre subprocesos, lo que puede afectar el rendimiento y la capacidad de respuesta del programa. Por lo tanto, al utilizar una sección crítica, es necesario sopesar sus ventajas y desventajas y tomar una decisión basada en la situación real.

En programación, existen muchas primitivas de sincronización diferentes (primitivas de sincronización) que pueden implementar funciones de secciones críticas, como mutex, bloqueos de lectura y escritura, semáforos, etc.

Concurrencia y Paralelismo

  1. La concurrencia se refiere a la ejecución alternativa de múltiples tareas dentro de un período de tiempo. Estas tareas pueden superponerse parcialmente, pero en realidad no se ejecutarán al mismo tiempo. En la era de las CPU de un solo núcleo, la concurrencia se logra mediante la rotación del intervalo de tiempo, es decir, la CPU ejecuta alternativamente múltiples tareas, cada tarea se ejecuta durante un período de tiempo y luego cambia a otra tarea. De esa manera, si bien cada tarea no se ejecuta realmente simultáneamente, desde la perspectiva del usuario parece que sí lo están haciendo.
  2. Paralelo significa que se ejecutan varias tareas al mismo tiempo. Esto requiere CPU de múltiples núcleos o soporte de entorno de subprocesos múltiples. El paralelismo puede mejorar en gran medida la eficiencia de ejecución del programa, especialmente cuando es necesario procesar una gran cantidad de cálculos o datos.
  3. Serial se refiere a la ejecución secuencial de múltiples tareas o métodos en un hilo. Esto significa que las tareas o métodos se ejecutan uno tras otro, sin partes superpuestas. La ejecución en serie es común en entornos de un solo subproceso, como programas que usan un solo subproceso o ciertas funciones o métodos que se ejecutan en serie.

cambio de contexto

El cambio de contexto es un concepto importante en la programación de subprocesos múltiples: significa que cuando se agota el intervalo de tiempo de un subproceso, la CPU guardará el estado del subproceso y luego cargará otro subproceso en el estado listo y lo ejecutará. Este proceso es un cambio de contexto.

El propósito del cambio de contexto es permitir que varios subprocesos compartan recursos de CPU y cada subproceso pueda obtener una cierta cantidad de tiempo de ejecución. Dado que un núcleo de CPU solo puede ser utilizado por un subproceso a la vez, la CPU adopta un enfoque de operación por turnos para asignar intervalos de tiempo a cada subproceso.

El proceso de cambio de contexto incluye guardar el estado del hilo actual (incluido el estado del registro, variables en la memoria, etc.) y cargar el estado del hilo en el siguiente estado listo. Este proceso lleva una cierta cantidad de tiempo y, a medida que aumenta la cantidad de subprocesos en el sistema, también aumenta la cantidad de cambios de contexto, lo que consume mucho tiempo de CPU.

En sistemas operativos como Linux, el consumo de tiempo del cambio de contexto y el cambio de modo es relativamente pequeño, lo que también es una razón importante para el excelente rendimiento de Linux. Esto se debe a que Linux utiliza muchas técnicas de optimización, como el uso del programador del kernel, el uso de interrupciones de hardware, etc., para reducir el consumo de tiempo del cambio de contexto y el cambio de modo.

Supongo que te gusta

Origin blog.csdn.net/m0_74293254/article/details/132507824
Recomendado
Clasificación