Una explicación detallada de la programación concurrente en un artículo.

Explicación detallada de la programación concurrente.

Recientemente estudié: "Comprensión profunda de la programación de alta concurrencia" de Binghe ; "El arte de la programación concurrente" ;
Por la presente, resuma brevemente el estudio para facilitar la mejora y consolidación posteriores del conocimiento de programación concurrente;
si desea comprender el estudio en En profundidad, puede leer el original de referencia anterior;

Hilos y grupos de hilos

proceso

El proceso es la unidad básica de asignación de recursos en el sistema. Cuando un sistema operativo moderno ejecuta un programa, crea un proceso para él.

El proceso es el espacio asignado por la aplicación en la memoria, que es el programa en ejecución, y cada proceso no interfiere entre sí.

Un sistema operativo que utiliza proceso + rotación de intervalos de tiempo de CPU puede manejar múltiples tareas al mismo tiempo a nivel macro, es decir, los procesos hacen posible la concurrencia del sistema operativo. Aunque la concurrencia se ve a un nivel macro, hay múltiples tareas en ejecución, pero de hecho, para una CPU de un solo núcleo, solo una tarea ocupa recursos de la CPU en un momento específico.

hilo

Los subprocesos son la unidad básica de programación de CPU. Es una unidad que es más pequeña que un proceso y se puede ejecutar de forma independiente.

En un proceso, se pueden crear múltiples subprocesos, que pueden tener sus propios contadores privados, memoria de pila y variables locales, y pueden acceder a variables de memoria compartida.

subprocesos múltiples

En el mismo programa, se pueden ejecutar varios subprocesos al mismo tiempo para realizar diferentes tareas; estos subprocesos pueden utilizar varios núcleos de la CPU para ejecutarse al mismo tiempo.

¿Por qué utilizar subprocesos múltiples? Lo más importante es: la programación multiproceso puede maximizar el uso de recursos de CPU multinúcleo.

cambio de contexto

El cambio de contexto se refiere al cambio de la CPU de un proceso (o subproceso) a otro proceso (o subproceso).

El contexto se refiere al contenido de los registros de la CPU y al contador del programa en un momento determinado.

El cambio de contexto suele ser computacionalmente intensivo, lo que significa que esta operación consume mucho tiempo de CPU, por lo que más subprocesos no siempre son mejores . Cómo reducir la cantidad de cambios de contexto en el sistema es un tema importante para mejorar el rendimiento de subprocesos múltiples.

Cómo se implementan los hilos

  1. Heredar la clase Thread

    public class ThreadDemo {
          
          
    
        public static class TestThread extends Thread {
          
          
            @Override
            public void run() {
          
          
                System.out.println("extends Thread");
            }
        }
    
        public static void main(String[] args) {
          
          
            TestThread testThread = new TestThread();
            testThread.start();
        }
    
        //console:extends Thread
    }
    
  2. Implementar la interfaz ejecutable

    public class ThreadDemo {
          
          
    
        public static class TestThreadRun implements Runnable {
          
          
            @Override
            public void run() {
          
          
                System.out.println("extends Thread2");
            }
        }
    
        public static void main(String[] args) {
          
          
            Thread thread = new Thread(new TestThreadRun());
            thread.start();
        }
    
        //console:extends Thread2
    }
    
  3. Implementar la interfaz invocable

    public class ThreadDemo {
          
          
    
        public static class TestThreadCall implements Callable<String> {
          
          
            @Override
            public String call() {
          
          
                System.out.println("extends Thread2");
                return "result:do success";
            }
        }
    
        public static void main(String[] args) {
          
          
            TestThreadCall call = new TestThreadCall();
            String result = call.call();
            System.out.println(result);
        }
    
        //console:
        // extends Thread2
        //result:do success
    }
    

Tarea prioritaria

En Java, la prioridad del subproceso se controla mediante una prioridad de variable miembro entera, que varía de 1 a 10. Cuanto mayor sea el valor, mayor será la prioridad. El valor predeterminado es 5.

Cuando a los subprocesos con alta prioridad se les asignan intervalos de tiempo de CPU, la probabilidad de asignación es mayor que la de los subprocesos con baja prioridad.

Nota: No se puede confiar en la prioridad del subproceso para la corrección del programa (no se garantiza que la prioridad alta se ejecute antes que la prioridad baja)

Orden de ejecución del hilo

En el mismo método, después de que se crean varios subprocesos continuamente, el orden en que se llama al método start () del subproceso no determina el orden de ejecución de los subprocesos.

¿Cómo garantizar el orden de ejecución de los hilos?

  1. Puede utilizar el método join() en la clase Thread para garantizar el orden de ejecución de los subprocesos. join() en realidad hace que el hilo principal espere a que el hilo secundario actual complete la ejecución .
  2. El método join () llamará internamente al método local wait (). Cuando se llama al método thread wait (), el subproceso principal estará en estado de espera, esperando a que el subproceso secundario complete la ejecución antes de ejecutar.

Ciclo de vida del hilo

Varios estados importantes en el ciclo de vida del hilo:

  1. NUEVO: Se crea el hilo, pero no se ha llamado al método start();

  2. EJECUTABLE: Estado ejecutable, incluido el estado listo y el estado en ejecución;

  3. BLOQUEADO: Estado bloqueado. Los subprocesos en este estado deben esperar a que otros subprocesos liberen el bloqueo o esperar para entrar sincronizados;

  4. ESPERANDO: Estado de espera. Los subprocesos en este estado deben esperar a que otros subprocesos se activen o interrumpan las operaciones antes de ingresar al siguiente estado;

    Llamar a los siguientes tres métodos pondrá el hilo en estado de espera:

    1. Object.wait(): pone el hilo actual en estado de espera hasta que otro hilo lo despierta
    2. Thread.join(): espera a que el hilo complete la ejecución. La llamada subyacente es el método wait() de Object.
    3. LockSupport.park(): deshabilita el hilo actual para la programación de hilos a menos que se conceda permiso para llamarlo.
  5. TIME_WAITTING: estado de espera de tiempo de espera, que puede regresar por sí solo después del tiempo especificado;

  6. TERMINADO: Estado de terminación, que indica que el hilo ha completado la ejecución;

Diagrama de flujo del ciclo de vida del hilo (abreviado):

Insertar descripción de la imagen aquí

Nota: Utilice jps combinado con el comando jstack para analizar en subprocesos la información de excepción de subprocesos de Java en el entorno de producción.

Comprensión profunda de la clase Thread

Definición de clase de hilo

public class Thread implements Runnable {
    
    ...}

@FunctionalInterface
public interface Runnable {
    
    
    public abstract void run();
}

La clase Thread implementa la interfaz Runnable, y la interfaz Rnnnable tiene solo un método run () y está modificada por la anotación @FunctionalInterface.

Cargar recursos locales

/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
    
    
    registerNatives();
}

Este método se utiliza principalmente para cargar algunos recursos locales y llamar a este método local en un bloque de código estático.

Variable miembro del hilo

 //线程名称
 private volatile String name;
 //优先级
 private int            priority;
 private Thread         threadQ;
 private long           eetop;

 /* Whether or not to single_step this thread. */
 //是否单步线程
 private boolean     single_step;

 /* Whether or not the thread is a daemon thread. */
 //是否守护线程
 private boolean     daemon = false;

 /* JVM state */
 private boolean     stillborn = false;

 /* What will be run. */
 //实际执行体
 private Runnable target;

 /* The group of this thread */
 private ThreadGroup group;

 /* The context ClassLoader for this thread */
 private ClassLoader contextClassLoader;

 /* The inherited AccessControlContext of this thread */
 //访问控制上下文
 private AccessControlContext inheritedAccessControlContext;

 /* For autonumbering anonymous threads. */
 //为匿名线程生成名称的编号
 private static int threadInitNumber;
 private static synchronized int nextThreadNum() {
    
    
     return threadInitNumber++;
 }

 /* ThreadLocal values pertaining to this thread. This map is maintained
  * by the ThreadLocal class. */
 //与此线程相关的ThreadLocal
 ThreadLocal.ThreadLocalMap threadLocals = null;

 /*
  * InheritableThreadLocal values pertaining to this thread. This map is
  * maintained by the InheritableThreadLocal class.
  */
 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

 //当前线程请求的堆栈大小
 private long stackSize;
 //线程终止后存在的JVM私有状态
 private long nativeParkEventPointer;
 //线程ID
 private long tid;

 /* For generating thread ID */
 //用于生成线程ID
 private static long threadSeqNumber;

 /* Java thread status for tools,
  * initialized to indicate thread 'not yet started'
  */
 //线程状态,初始为0,表示未启动
 private volatile int threadStatus = 0;

 /**
  * The argument supplied to the current call to
  * java.util.concurrent.locks.LockSupport.park.
  * Set by (private) java.util.concurrent.locks.LockSupport.setBlocker
  * Accessed using java.util.concurrent.locks.LockSupport.getBlocker
  */
 volatile Object parkBlocker;

 /* The object in which this thread is blocked in an interruptible I/O
  * operation, if any.  The blocker's interrupt method should be invoked
  * after setting this thread's interrupt status.
  */
 //Interruptible中定义了中断方法,用来中断特定线程
 private volatile Interruptible blocker;
 //当前线程的内部锁
 private final Object blockerLock = new Object();
 //线程最小优先级
 public final static int MIN_PRIORITY = 1;
 //默认优先级
 public final static int NORM_PRIORITY = 5;
 //最大优先级
 public final static int MAX_PRIORITY = 10;

Se puede ver en las variables miembro que la clase Thread no es una tarea, sino un objeto de hilo real, y la variable de destino del tipo Runnable dentro de ella es la tarea real.

Definición del estado del hilo

public enum State {
    
    
    /**
     * Thread state for a thread which has not yet started.
     */
    //新建状态;线程被创建,但是还没有调用start()方法
    NEW,

    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */
    //可运行状态;包括运行中状态和就绪状态
    RUNNABLE,

    //阻塞状态;此状态的线程需要等待其他线程释放锁,或者等待进入synchronized
    BLOCKED,

    //等待状态;此状态的线程需要其他线程对其进行唤醒或者中断状态,进而进入下一状态
    WAITING,

    //超时等待状态;可以在一定的时间自行返回
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
    //终止状态;当前线程执行完毕
    TERMINATED;
}

Método constructor de la clase Thread

public Thread() {
    
    
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

public Thread(Runnable target) {
    
    
        init(null, target, "Thread-" + nextThreadNum(), 0);
}

public Thread(ThreadGroup group, Runnable target) {
    
    
        init(group, target, "Thread-" + nextThreadNum(), 0);
}

public Thread(String name) {
    
    
        init(null, null, name, 0);
}

public Thread(ThreadGroup group, String name) {
    
    
        init(group, null, name, 0);
}

public Thread(ThreadGroup group, Runnable target, String name) {
    
    
        init(group, target, name, 0);
}

A través de varios métodos de construcción de la clase Thread de uso común, descubrimos que la inicialización de la clase Thread se logra principalmente a través del método init ().

método inicial()

/**
 * Initializes a Thread.
 *
 * @param g the Thread group
 * @param target the object whose run() method gets called
 * @param name the name of the new Thread
 * @param stackSize the desired stack size for the new thread, or
 *        zero to indicate that this parameter is to be ignored.
 * @param acc the AccessControlContext to inherit, or
 *            AccessController.getContext() if null
 * @param inheritThreadLocals if {@code true}, inherit initial values for
 *            inheritable thread-locals from the constructing thread
 */
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    
    
    if (name == null) {
    
    
        //名称为空
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;

    Thread parent = currentThread();
    //安全管理器
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
    
    
        /* Determine if it's an applet or not */

        /* If there is a security manager, ask the security manager
           what to do. */
        if (security != null) {
    
    
            //获取线程组
            g = security.getThreadGroup();
        }

        /* If the security doesn't have a strong opinion of the matter
           use the parent thread group. */
        if (g == null) {
    
    
            //线程组为空,从父类获取
            g = parent.getThreadGroup();
        }
    }

    /* checkAccess regardless of whether or not threadgroup is
       explicitly passed in. */
    g.checkAccess();

    /*
     * Do we have the required permissions?
     */
    if (security != null) {
    
    
        if (isCCLOverridden(getClass())) {
    
    
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();
    //当前线程继承父线程相关属性
    this.group = g;
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    /* Stash the specified stack size in case the VM cares */
    this.stackSize = stackSize;

    /* Set thread ID */
    tid = nextThreadID();
}

El método de construcción de la clase Thread es llamado por el hilo principal que crea el hilo Thread. En este momento, el hilo principal que llama al método de construcción es el hilo principal de Thread; en el método init (), el hilo Thread recién creado heredará parte del hilo principal.

método ejecutar()

@Override
public void run() {
    
    
    if (target != null) {
    
    
        target.run();
    }
}

Se puede ver que la implementación del método run () del método Thread es relativamente simple: en realidad se implementa ejecutando el método run () en un objetivo de tipo Runnable;

Cabe señalar que llamar directamente al método de ejecución de la interfaz Runnable no creará un nuevo hilo para realizar la tarea; si necesita crear un nuevo hilo para realizar la tarea, debe llamar al método start() del Thread. clase;

método inicio()

public synchronized void start() {
    
    
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);
    //标记线程是否启动
    boolean started = false;
    try {
    
    
        //调用本地方法启动
        start0();
        //变更线程启动标识
        started = true;
    } finally {
    
    
        try {
    
    
            if (!started) {
    
    
                //未启动,则线程组中标记为启动失败
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
    
    
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

Se puede ver en el código fuente que el método start () se modifica mediante sincronizado, lo que indica que este método es sincrónico. Verificará el estado del hilo antes de que el hilo se inicie realmente. Si no es 0 (estado NUEVO) , devolverá directamente una excepción, por lo que un hilo A solo se puede iniciar una vez. Si se inicia varias veces, se informará una excepción ;

Después de llamar al método start(), el subproceso recién creado estará en el estado listo (si no está programado por la CPU). Cuando la CPU esté inactiva, la CPU lo programará para su ejecución. En este momento, el El subproceso está en estado de ejecución y la JVM llamará al método run() del subproceso para realizar tareas.

método dormir()

public static native void sleep(long millis) throws InterruptedException;

public static void sleep(long millis, int nanos) throws InterruptedException {
    
    
    if (millis < 0) {
    
    
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
    
    
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }

    if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
    
    
        millis++;
    }

    sleep(millis);
}

El método sleep() hará que el hilo entre en suspensión durante un período de tiempo. Cabe señalar que el bloqueo no se liberará después de llamar al método sleep() para poner el hilo en suspensión.

método unirse ()

El escenario de uso del método join () suele ser el hilo que inicia el hilo para ejecutar la tarea, llama al método join () del hilo de ejecución y espera a que el hilo de ejecución ejecute la tarea hasta que se agote el tiempo de espera o la ejecución. El hilo termina.

método de interrupción ()

Interrupción del hilo:

En algunos casos, después de iniciar el hilo, descubrimos que no necesitamos ejecutarlo y necesitamos interrumpir este hilo. Actualmente no existe una forma segura y directa de detener un subproceso en JAVA, pero JAVA introduce un mecanismo de interrupción de subprocesos para manejar situaciones en las que es necesario interrumpir los subprocesos.

El mecanismo de interrupción del hilo es un mecanismo cooperativo. Cabe señalar que la operación de interrupción no finaliza directamente el subproceso en ejecución, sino que notifica al subproceso interrumpido que lo maneje por sí solo.

Se proporcionan varios métodos en la clase Thread para manejar las interrupciones de subprocesos:

  1. Thread.interrupt(): Interrumpe el hilo. El hilo de interrupción aquí no finaliza inmediatamente el hilo en ejecución, sino que establece el indicador de interrupción del hilo en verdadero.
  2. Thread.currentThread.isInterrupt(): prueba si el hilo actual está interrumpido. El estado de interrupción del subproceso se ve afectado por este método, lo que significa que llamarlo una vez establecerá el estado de interrupción del subproceso en verdadero, y llamarlo dos veces restablecerá el estado de interrupción del subproceso a falso.
  3. Thread.isInterrupt(): prueba si el hilo actual está interrumpido. A diferencia del método anterior, este método no cambia el estado de interrupción del hilo.

Este método se utiliza para interrumpir la ejecución del hilo actual e interrumpe el hilo actual configurando el bit de bandera de interrupción del hilo. En este momento, si el bit de bandera de interrupción está configurado para el hilo,

Se puede generar una InterruptException y se borrará el estado de interrupción del hilo actual. Esta forma de interrumpir el hilo es más segura: permite que la tarea que se está ejecutando continúe completándose, a diferencia de detener, que obliga a cerrar el hilo.

public void interrupt() {
    
    
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
    
    
        Interruptible b = blocker;
        if (b != null) {
    
    
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

Suspender()/reanudar()/detener() caducado

Los tres métodos suspender()/resume()/stop() pueden entenderse simplemente como suspender, reanudar y finalizar subprocesos. Dado que los métodos están desactualizados, no se recomienda su uso.

Invocable y futuro

Interfaz invocable

La interfaz Callable puede obtener el resultado devuelto después de la ejecución del subproceso, mientras que heredar Thread e implementar la interfaz Runnable no puede obtener el resultado de la ejecución.

@FunctionalInterface
public interface Runnable {
    
    
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}


@FunctionalInterface
public interface Callable<V> {
    
    
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

Se puede ver que la interfaz Callable es similar a la interfaz Runnable, también es una interfaz funcional con un solo método, la diferencia es que el método proporcionado por Callable tiene un valor de retorno y admite genéricos.

Callable se usa generalmente junto con la clase ExecutorService de la subherramienta de subprocesos. Profundizaremos en el uso de grupos de subprocesos en capítulos posteriores.

Aquí solo presentamos que ExecutorService puede llamar al método de envío para ejecutar un Callable y devolver un Future. Los programas posteriores pueden obtener el resultado de la ejecución a través del método get de Future.

public class TestTask implements Callable<String> {
    
    
    @Override
    public String call() throws Exception {
    
    
        //模拟程序执行需要一秒
        Thread.sleep(1000);
        return "do success!";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        ExecutorService executorService = Executors.newCachedThreadPool();
        TestTask testTask = new TestTask();
        Future<String> submitRes = executorService.submit(testTask);

        //注意调用get方法会阻塞当前线程,知道得到结果
        //实际编码中建议使用设有超时时间的重载get方法
        String reslut = submitRes.get();
        System.out.println(reslut);
    }
    
    //console:
    // do success!
}

modelo asincrónico

  1. Modelo asincrónico sin resultado de retorno.

    Las tareas asincrónicas que no devuelven resultados se pueden lanzar directamente a subprocesos o grupos de subprocesos para su ejecución. En este momento, los resultados de la ejecución de la tarea no se pueden obtener directamente. Una forma es obtener los resultados de ejecución a través del método de devolución de llamada; el método de implementación es similar al modo observador.

  2. Modelos asincrónicos que devuelven resultados.

    El JDK proporciona una solución que puede obtener y devolver directamente resultados asincrónicos:

    1. Utilice Future para obtener resultados

      La interfaz Future se utiliza a menudo junto con el grupo de subprocesos para obtener resultados asincrónicos.

    2. Obtenga resultados usando FutureTask

      La clase FutureTask se puede utilizar junto con la clase Thread o con el grupo de subprocesos.

Interfaz futura

  1. Interfaz futura

    public interface Future<V> {
          
          
        
        boolean cancel(boolean mayInterruptIfRunning);
    
        boolean isCancelled();
    
        boolean isDone();
       
        V get() throws InterruptedException, ExecutionException;
        
        V get(long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeotException;
    
    1. cancelación booleana (booleana)
      1. Cancela la ejecución de la tarea, recibiendo un parámetro de tipo booleano, y devuelve verdadero si la cancelación es exitosa, en caso contrario devuelve falso.
      2. La tarea se ha completado, finalizado o no se puede cancelar. Devuelve falso, lo que indica que la cancelación falló.
      3. Si la tarea no se ha iniciado, llamar a este método devuelve verdadero, lo que indica una cancelación exitosa.
      4. Si la tarea se ha iniciado, se determinará en función del parámetro booleano de entrada si se debe cancelar la tarea interrumpiendo el hilo.
    2. booleano está cancelado()
      1. La tarea de juicio se cancela antes de completarse
      2. Si la tarea se cancela antes de completarse, devuelve verdadero; de lo contrario, devuelve falso
      3. Nota: Verdadero se devolverá solo si la tarea no se inicia y cancela antes de su finalización; de lo contrario, devolverá falso.
    3. booleano está hecho()
      1. Determinar si la tarea se ha completado.
      2. Si la tarea finaliza normalmente, sale con una excepción o se cancela, devolverá verdadero, lo que indica que la tarea se completó.
    4. V obtener()
      1. Cuando se completa la tarea, los datos del resultado de la tarea se devuelven directamente.
      2. Cuando no se complete, espere a que se complete la tarea y devuelva el resultado
    5. V get(largo,Unidad de tiempo)
      1. Cuando se completa la tarea, el resultado de finalización de la tarea se devuelve directamente.
      2. Si no se completa, esperará el resultado devuelto dentro del período de tiempo de espera. Si se excede el tiempo, se generará una excepción TimeOutException.
  2. Interfaz RunnableFuture

    public interface RunnableFuture<V> extends Runnable, Future<V> {
          
          
        /**
         * Sets this Future to the result of its computation
         * unless it has been cancelled.
         */
        void run();
    }
    

    La interfaz RunnabeleFuture no solo hereda la interfaz Future, sino que también hereda la interfaz Runnable, por lo que tiene las interfaces abstractas de ambas.

  3. Clase de tarea futura

    public class FutureTask<V> implements RunnableFuture<V> {
          
          
        //详细源码,后续分析
    }
    

    La clase FutureTask es una clase de implementación muy importante de la interfaz RunnableFuture: implementa todos los métodos abstractos de la interfaz RunnableFuture, la interfaz Future y la interfaz Runnnable.

    1. Variables y constantes en la clase FutureTask

      Primero, se define un estado de variable modificada volátil. Las variables volátiles logran la seguridad de los subprocesos a través de barreras de memoria y prohíben el reordenamiento;

      Luego defina varias constantes de estado cuando la tarea se esté ejecutando;

      (En los comentarios del código, se dan varios cambios de estado posibles)

      	/* Possible state transitions:
           * NEW -> COMPLETING -> NORMAL
           * NEW -> COMPLETING -> EXCEPTIONAL
           * NEW -> CANCELLED
           * NEW -> INTERRUPTING -> INTERRUPTED
           */
          private volatile int state;
          private static final int NEW          = 0;
          private static final int COMPLETING   = 1;
          private static final int NORMAL       = 2;
          private static final int EXCEPTIONAL  = 3;
          private static final int CANCELLED    = 4;
          private static final int INTERRUPTING = 5;
          private static final int INTERRUPTED  = 6;
      

      A continuación, se definen varias variables miembro;

      	/** The underlying callable; nulled out after running */
          private Callable<V> callable;
          /** The result to return or exception to throw from get() */
          private Object outcome; // non-volatile, protected by state reads/writes
          /** The thread running the callable; CASed during run() */
          private volatile Thread runner;
          /** Treiber stack of waiting threads */
          private volatile WaitNode waiters;
      
      1. invocable: realiza tareas específicas llamando al método run();
      2. outcaom: el resultado devuelto o información de excepción obtenida mediante el método get()
      3. corredor: el hilo utilizado para ejecutar la interfaz invocable y garantizar la seguridad del hilo a través de CAS
      4. camareros: la pila de subprocesos en espera. En las clases derivadas, CAS y esta pila se utilizarán para cambiar el estado de ejecución.
    2. Método de construcción

      Dos métodos de construcción diferentes para pasar parámetros;

      
          public FutureTask(Callable<V> callable) {
              
              
              if (callable == null)
                  throw new NullPointerException();
              this.callable = callable;
              this.state = NEW;       // ensure visibility of callable
          }
      
          /**
           * Creates a {@code FutureTask} that will, upon running, execute the
           * given {@code Runnable}, and arrange that {@code get} will return the
           * given result on successful completion.
           *
           * @param runnable the runnable task
           * @param result the result to return on successful completion. If
           * you don't need a particular result, consider using
           * constructions of the form:
           * {@code Future<?> f = new FutureTask<Void>(runnable, null)}
           * @throws NullPointerException if the runnable is null
           */
          public FutureTask(Runnable runnable, V result) {
              
              
              this.callable = Executors.callable(runnable, result);
              this.state = NEW;       // ensure visibility of callable
          }
      
    3. está cancelado () y está hecho ()

          public boolean isCancelled() {
              
              
              return state >= CANCELLED;
          }
      
          public boolean isDone() {
              
              
              return state != NEW;
          }
      

      En ambos métodos, si se ha cancelado o completado se determina juzgando el tamaño del valor del estado;

      Lo que se puede aprender aquí es que al definir valores de estado en el futuro, intente seguir ciertas reglas de cambio. De esta manera, para escenarios donde el estado cambia con frecuencia, los valores de estado regulares pueden tener el efecto de obtener el doble de resultado con la mitad del esfuerzo al realizar la lógica empresarial.

    4. método cancelar (booleano)

      public boolean cancel(boolean mayInterruptIfRunning) {
              
              
              if (!(state == NEW &&
                    UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                        mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
                  return false;
              try {
              
                  // in case call to interrupt throws exception
                  if (mayInterruptIfRunning) {
              
              
                      try {
              
              
                          Thread t = runner;
                          if (t != null)
                              t.interrupt();
                      } finally {
              
               // final state
                          UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                      }
                  }
              } finally {
              
              
                  finishCompletion();
              }
              return true;
          }
      

      En primer lugar, se juzgará rápidamente si se puede cancelar en función del juicio de estado o del resultado de la operación CAS; si el estado no es NUEVO o CAS devuelve falso, se devolverá directamente la falla de cancelación;

      Luego, en el bloque de código de prueba, primero determine si se puede interrumpir para cancelar; si es así, defina una referencia que apunte a la tarea en ejecución y determine si la tarea está vacía. Si no, llame al método de interrupción y luego modifique la ejecución estado para cancelar;

      Finalmente, llame al método FinishCompletion() en el bloque de código finalmente para finalizar la ejecución de la tarea;

      /**
           * Removes and signals all waiting threads, invokes done(), and
           * nulls out callable.
           */
          private void finishCompletion() {
              
              
              // assert state > COMPLETING;
              for (WaitNode q; (q = waiters) != null;) {
              
              
                  if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
              
              
                      for (;;) {
              
              
                          Thread t = q.thread;
                          if (t != null) {
              
              
                              q.thread = null;
                              LockSupport.unpark(t);
                          }
                          WaitNode next = q.next;
                          if (next == null)
                              break;
                          q.next = null; // unlink to help gc
                          q = next;
                      }
                      break;
                  }
              }
              done();
              callable = null;        // to reduce footprint
          }
      

      En el método FinishCompletion (), primero se define un bucle for, bucle de camareros (pila de subprocesos en espera), y la condición de terminación del bucle es que los camareros estén vacíos; dentro del bucle específico, primero determine si la operación CAS fue exitosa y si es exitosa. , defina un nuevo bucle de giro; en el bucle de giro, el hilo en la pila se despertará para completar su operación. Una vez completada, la interrupción saltará fuera del bucle y, finalmente, se llamará al método done() y se llamará al invocable. estar vacío;

    5. método obtener()

      
          public V get() throws InterruptedException, ExecutionException {
              
              
              int s = state;
              if (s <= COMPLETING)
                  s = awaitDone(false, 0L);
              return report(s);
          }
      
          public V get(long timeout, TimeUnit unit)
              throws InterruptedException, ExecutionException, TimeoutException {
              
              
              if (unit == null)
                  throw new NullPointerException();
              int s = state;
              if (s <= COMPLETING &&
                  (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
                  throw new TimeoutException();
              return report(s);
          }
      

      El método get() sin parámetros se bloqueará y esperará a que se complete la tarea cuando la tarea no se complete; el método get() con parámetros se bloqueará y esperará a que se complete, pero después de que se exceda el período de tiempo dado, se generará una TimeoutException. arrojado;

      /**
           * Awaits completion or aborts on interrupt or timeout.
           *
           * @param timed true if use timed waits
           * @param nanos time to wait, if timed
           * @return state upon completion
           */
          private int awaitDone(boolean timed, long nanos)
              throws InterruptedException {
              
              
              final long deadline = timed ? System.nanoTime() + nanos : 0L;
              WaitNode q = null;
              boolean queued = false;
              for (;;) {
              
              
                  if (Thread.interrupted()) {
              
              
                      removeWaiter(q);
                      throw new InterruptedException();
                  }
      
                  int s = state;
                  if (s > COMPLETING) {
              
              
                      if (q != null)
                          q.thread = null;
                      return s;
                  }
                  else if (s == COMPLETING) // cannot time out yet
                      Thread.yield();
                  else if (q == null)
                      q = new WaitNode();
                  else if (!queued)
                      queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                           q.next = waiters, q);
                  else if (timed) {
              
              
                      nanos = deadline - System.nanoTime();
                      if (nanos <= 0L) {
              
              
                          removeWaiter(q);
                          return state;
                      }
                      LockSupport.parkNanos(this, nanos);
                  }
                  else
                      LockSupport.park(this);
              }
          }
      

      El método awaitDone() espera principalmente a que se complete o se interrumpa la ejecución;

      El más importante es el bucle for spin. En el bucle, primero determinará si se interrumpe. Si se interrumpe, se llama a removeWaiter() para eliminar la pila en espera y generar una excepción de interrupción; si no se interrumpe, la lógica se ejecuta para determinar si se completa;

    6. establecer() y establecerExcepción()

      
          protected void set(V v) {
              
              
              if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
              
              
                  outcome = v;
                  UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
                  finishCompletion();
              }
          }
      
          protected void setException(Throwable t) {
              
              
              if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
              
              
                  outcome = t;
                  UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL);
                  finishCompletion();
              }
          }
      

      La lógica de los dos métodos es casi la misma, excepto que uno se establece en NORMAL al configurar el estado de la tarea y el otro se establece en EXCEPCIONAL;

    7. ejecutar() y ejecutarAndReset()

      public void run() {
              
              
              if (state != NEW ||
                  !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                               null, Thread.currentThread()))
                  return;
              try {
              
              
                  Callable<V> c = callable;
                  if (c != null && state == NEW) {
              
              
                      V result;
                      boolean ran;
                      try {
              
              
                          result = c.call();
                          ran = true;
                      } catch (Throwable ex) {
              
              
                          result = null;
                          ran = false;
                          setException(ex);
                      }
                      if (ran)
                          set(result);
                  }
              } finally {
              
              
                  // runner must be non-null until state is settled to
                  // prevent concurrent calls to run()
                  runner = null;
                  // state must be re-read after nulling runner to prevent
                  // leaked interrupts
                  int s = state;
                  if (s >= INTERRUPTING)
                      handlePossibleCancellationInterrupt(s);
              }
          }
      

      Se puede decir que si se usa Future o FutureTask (), inevitablemente se llamará al método run () para ejecutar la tarea;

      En el método run (), primero determinará si está en el estado NUEVO o si la operación CAS devuelve falso, y regresará directamente sin continuar con la ejecución;

      En el siguiente bloque de código de prueba, se ejecuta el método call() del invocable y se recibe el resultado;

    8. método removeWaiter()

      
          private void removeWaiter(WaitNode node) {
              
              
              if (node != null) {
              
              
                  node.thread = null;
                  retry:
                  for (;;) {
              
                        // restart on removeWaiter race
                      for (WaitNode pred = null, q = waiters, s; q != null; q = s){
              
              
                          s = q.next;
                          if (q.thread != null)
                              pred = q;
                          else if (pred != null) {
              
              
                              pred.next = s;
                              if (pred.thread == null) // check for race
                                  continue retry;
                          }
                          else if (!UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                                q, s))
                              continue retry;
                      }
                      break;
                  }
              }
          }
      

      Este método elimina principalmente el hilo en WaitNode (pila de espera) a través de un bucle giratorio;

Análisis en profundidad de ThreadPoolExecutor

La subtecnología de subprocesos de Java es una de las tecnologías centrales de Java y, en el campo de la alta concurrencia de Java, es un tema que nunca se puede evitar.

Desventajas de Thread que crea hilos directamente

  1. Cada vez que se utiliza un nuevo hilo (), se crea un nuevo hilo, sin reutilización y rendimiento deficiente;
  2. Los subprocesos carecen de una administración unificada y se pueden crear nuevos subprocesos sin restricciones, lo que puede ocupar una gran cantidad de recursos y provocar OOM o fallas;
  3. Falta de más operaciones de control, como más ejecuciones, ejecuciones regulares, interrupciones, etc.;

Beneficios de usar el grupo de subprocesos

  1. Los subprocesos existentes se pueden reutilizar, lo que reduce la sobrecarga causada por subprocesos nuevos y mejora el rendimiento;
  2. Puede controlar eficazmente la cantidad máxima de concurrencias, mejorar la utilización de recursos y reducir la competencia de recursos causada por demasiados subprocesos;
  3. Proporciona funciones como ejecución programada, ejecución periódica, subproceso único y control de concurrencia;
  4. Proporcionar métodos para admitir la supervisión del grupo de subprocesos, que pueden monitorear el estado de ejecución del grupo de subprocesos en tiempo real;

Grupo de subprocesos

  1. Ejecutores
    1. newCachedThreadPool: cree un grupo de subprocesos almacenable en búfer. Si el tamaño del grupo de subprocesos excede las necesidades de procesamiento, los subprocesos inactivos se pueden reciclar de manera flexible. Si no hay subprocesos para reciclar, se creará un nuevo subproceso;
    2. newFixedThreadPool: crea un grupo de subprocesos de longitud fija, que puede controlar el número máximo de subprocesos concurrentes. Los subprocesos que excedan la longitud fija esperarán en la cola;
    3. newScheduledThreadPool: crea un grupo de subprocesos de longitud fija que se puede ejecutar de forma regular y periódica;
    4. newSingleThreadPool: crea un grupo de subprocesos de un solo subproceso y utiliza un subproceso único para ejecutar tareas de subprocesos, asegurando que todas las tareas se ejecuten en el orden especificado;
    5. newSingleScheduleThreadPool: crea un grupo de subprocesos de un solo subproceso que se puede ejecutar de forma regular y periódica;
    6. newWorkStealingThreadPool: crea un grupo de subprocesos de robo de trabajo con nivel paralelo;

Varios estados de instancias de grupo de subprocesos

  1. Correr

    Estado de ejecución, puede aceptar tareas recién enviadas y también puede procesar tareas en la cola de bloqueo

  2. Cerrar

    En el estado cerrado, ya no acepta tareas recién enviadas, pero puede procesar tareas guardadas en la cola de bloqueo;

    Ejecutando --> apagado() --> Apagado

  3. Detener

    En el estado detenido, no se pueden recibir nuevas tareas y no se pueden procesar las tareas en la cola de bloqueo, lo que interrumpirá el procesamiento de las tareas;

    Ejecutando/Apagando --> apagadoAhora() --> Detener

  4. ordenar

    Estado claro, todas las tareas han finalizado, el número de subprocesos efectivos es 0 (el número de subprocesos en la cola de bloqueo es 0 y el número de subprocesos que se ejecutan en el grupo de subprocesos también es 0)

  5. Terminado

    Estado final, Ordenando --> terminar() --> Terminado

Nota: No es necesario realizar un procesamiento especial para el estado del grupo de subprocesos. El estado del grupo de subprocesos lo define y procesa internamente el grupo de subprocesos de acuerdo con el método.

Sugerencias para configurar correctamente la cantidad de subprocesos

  1. Para tareas que requieren un uso intensivo de la CPU y requieren exprimir la CPU, la cantidad de subprocesos se puede configurar en ncpu + 1 (cantidad de CPU + 1)
  2. Para tareas intensivas de IO, el número se puede configurar en ncpu*2 (2 veces el número de CPU)

ThreadPoolExecutor, la clase principal del grupo de subprocesos

  1. Método de construcción

    /**
         * Creates a new {@code ThreadPoolExecutor} with the given initial
         * parameters and default thread factory and rejected execution handler.
         * It may be more convenient to use one of the {@link Executors} factory
         * methods instead of this general purpose constructor.
         *
         * @param corePoolSize the number of threads to keep in the pool, even
         *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
         * @param maximumPoolSize the maximum number of threads to allow in the
         *        pool
         * @param keepAliveTime when the number of threads is greater than
         *        the core, this is the maximum time that excess idle threads
         *        will wait for new tasks before terminating.
         * @param unit the time unit for the {@code keepAliveTime} argument
         * @param workQueue the queue to use for holding tasks before they are
         *        executed.  This queue will hold only the {@code Runnable}
         *        tasks submitted by the {@code execute} method.
         * @throws IllegalArgumentException if one of the following holds:<br>
         *         {@code corePoolSize < 0}<br>
         *         {@code keepAliveTime < 0}<br>
         *         {@code maximumPoolSize <= 0}<br>
         *         {@code maximumPoolSize < corePoolSize}
         * @throws NullPointerException if {@code workQueue} is null
         */
        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue) {
          
          
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                 Executors.defaultThreadFactory(), defaultHandler);
        }
    

    Este es el constructor con más parámetros y otros constructores están sobrecargados con este método.

    1. corePoolSize: número de subprocesos centrales
    2. MaximumPoolSize: número máximo de subprocesos
    3. workQueue: cola de bloqueo, almacenamiento de tareas en espera de ser ejecutadas

    La relación entre los tres parámetros anteriores es la siguiente:

    • Si el número de subprocesos en ejecución es menor que corePoolSize, se creará un nuevo subproceso directamente para ejecutarse, incluso si otros subprocesos en el grupo de subprocesos están inactivos.
    • Si el número de subprocesos en ejecución es mayor o igual que corePoolSize y menor que MaximumPoolSize, el número excedente ingresará a workQueue y esperará. Solo cuando workQueue esté lleno se creará un nuevo subproceso.
    • Si corePoolSize es igual a MaximumPoolSize, entonces el tamaño del grupo de subprocesos es fijo en este momento. Si se envía una nueva tarea y workQueue no está lleno, ingresará a workQueue y esperará a que se retire un subproceso inactivo de workQueue para ejecución.
    • Si el número de subprocesos en ejecución excede el tamaño máximo de grupo y la cola de trabajo está llena, la política se procesará a través de la política de rechazo rechazoHandler

    Según los parámetros anteriores, el hilo procesará la tarea de la siguiente manera:

    Cuando se envía una nueva tarea al grupo de subprocesos, el grupo de subprocesos realizará diferentes métodos de procesamiento según la cantidad de subprocesos en ejecución actualmente; hay tres métodos de procesamiento principales: conmutación directa, uso de colas ilimitadas y uso de colas limitadas.

    • Cambiar directamente la cola de uso común es SynchronousQueue
    • Usar una cola infinita significa usar una cola basada en una lista vinculada. Por ejemplo, LinkedBlockingQueue, cuando se usa esta cola, el número máximo de subprocesos creados en el grupo de subprocesos es corePoolSize, maxPoolSize no funcionará
    • Usar una cola limitada significa usar una cola basada en matrices. Por ejemplo, ArrayBlockingQueue, utilizando este método, puede limitar el número máximo de subprocesos en el grupo de subprocesos al máximoPoolSize, lo que puede reducir el consumo de recursos; pero este método hace que sea más difícil programar subprocesos, porque el número de grupos de subprocesos y el número de ambas colas son fijas

    Con base en los parámetros anteriores, podemos simplemente dibujar algunas medidas para reducir el consumo de recursos:

    • Si desea reducir el consumo de recursos del sistema, la sobrecarga de cambio de contexto, etc., puede establecer una capacidad de cola más grande y una capacidad de grupo de subprocesos más pequeña, lo que reducirá el rendimiento de las tareas de procesamiento de subprocesos.
    • Si las tareas enviadas se bloquean con frecuencia, puede restablecer el número máximo de subprocesos a un número mayor.
    1. keepAliveTime: cuando el hilo no está ejecutando tareas, el tiempo máximo de espera para la terminación

      Cuando la cantidad de subprocesos en el grupo de subprocesos excede corePoolSize, si no se envían nuevas tareas, los subprocesos que exceden la cantidad de subprocesos principales no se destruirán inmediatamente, sino que se destruirán después de esperar keepAliveTime.

    2. unidad: unidad de tiempo de keepAliveTime

    3. threadFactory: fábrica de hilos, utilizada para crear hilos

      De forma predeterminada, se creará una fábrica de subprocesos. Los subprocesos creados por la fábrica predeterminada tienen la misma prioridad y no son demonios. También se establece el nombre del subproceso.

    4. rechazarHandler: estrategia de rechazo

      Si workQueue está lleno y no hay subprocesos inactivos, se ejecutará la política de rechazo.

      El grupo de subprocesos proporciona un total de cuatro estrategias de rechazo:

      • Lanza una excepción directamente, que también es la estrategia predeterminada. La clase de implementación es AbortPolicy
      • Utilice el hilo de la persona que llama para ejecutar y la clase de implementación es CallerRunsPolicy
      • Descarte la tarea más importante en la cola y ejecute la tarea actual. La clase de implementación es DiscardOldestPolicy
      • Descarte directamente la tarea actual, la clase de implementación es DiscardPolicy
  2. Cómo empezar y parar

    1. ejecutar (): envía la tarea al grupo de subprocesos para su ejecución
    2. enviar (): envía una tarea y puede devolver resultados, equivalente a ejecutar + futuro
    3. ShutDown(): cierra el grupo de subprocesos y espera a que el subproceso complete su ejecución.
    4. ShutDownNow(): cierra el grupo de subprocesos inmediatamente sin esperar a que el subproceso complete su ejecución.
  3. Métodos adecuados para el seguimiento.

    1. getTaskCount(): obtiene el número total de subprocesos ejecutados y no ejecutados
    2. getCompletedTaskCount(): obtiene el número de subprocesos que han completado la ejecución
    3. getCorePoolSize(): obtiene el número de subprocesos principales
    4. getActiveCount(): u obtiene el número de subprocesos en ejecución

Análisis en profundidad de interfaces de nivel superior y clases abstractas en grupos de subprocesos

Descripción general de interfaces y clases abstractas

Insertar descripción de la imagen aquí

  • Interfaz del ejecutor: la interfaz de nivel superior del grupo de subprocesos, que proporciona un método para enviar tareas sin un valor de retorno.
  • ExecutorService: heredado de Executor, extiende muchas funciones, como cerrar el grupo de subprocesos, enviar tareas y devolver resultados, activar tareas en el grupo de subprocesos, etc.
  • AbstractExecutotService: hereda de ExecutorService e implementa algunos métodos muy prácticos para que las subclases llamen
  • ScheduledExecutorService: hereda de ExecutorService y extiende métodos relacionados con tareas programadas

Interfaz de ejecutor

public interface Executor {
    
    
    void execute(Runnable command);
}

La interfaz Executor es relativamente simple y proporciona un método de ejecución (comando ejecutable) para ejecutar las tareas enviadas.

Interfaz de servicio ejecutor

La interfaz ExecutorService es la interfaz principal del grupo de subprocesos de tareas no programadas. A través de esta interfaz, las tareas se envían al grupo de subprocesos (admite métodos de retorno y no retorno), cierran el grupo de subprocesos, activan tareas de subprocesos, etc.

public interface ExecutorService extends Executor {
    
    
    //关闭线程池,不再接受新提交的任务,但之前提交的任务继续执行,直到完成
    void shutdown();

    //立即关闭线程池,不再接受新提交任务,会尝试停止线程池中正在执行的任务
    List<Runnable> shutdownNow();

    //判断线程池是否已经关闭
    boolean isShutdown();

    //判断线程中所有任务是否已经结束,只有调用shutdown()或shutdownNow()之后,调用此方法才返回true
    boolean isTerminated();

    //等待线程中所有任务执行结束,并设置超时时间
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;
        
    //提交一个Callable类型的任务,并返回一个Future类型的结果
    <T> Future<T> submit(Callable<T> task);

    //提交一个Runnable类型任务,并设置泛型接收结果数据,返回一个Futrue类型结果
    <T> Future<T> submit(Runnable task, T result);

    //提交一个Runnable类型任务,并返回一个Future类型结果
    Future<?> submit(Runnable task);

    //批量提交Callable类型任务,并返回他们的执行结果,Task列表和Future列表一一对应
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
        
    //批量提交Callable类型任务,并获取返回结果,并限定处理所有任务的时间
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;

    //批量提交任务,并获得一个已经成功执行任务的结果
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;

    //批量提交任务,并获得一个已经完成任务的结果,并限定处理任务时间
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

ResumenEjecutorEservice

Esta clase es una clase abstracta que hereda de ExecutorEservice y, sobre esta base, implementa algunos métodos prácticos para que las subclases los llamen.

1. nuevaTareaPara()


    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    
    
        return new FutureTask<T>(runnable, value);
    }

    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    
    
        return new FutureTask<T>(callable);
    }

FutureTask se utiliza para obtener resultados de ejecución. En aplicaciones prácticas, a menudo utilizamos su subclase FutureTask;

La función del método newTaskFor es encapsular la tarea en un objeto FutureTask y luego enviar el objeto FutureTask al grupo de subprocesos.

2. hacerInvocarAny()

Este método es un método central que ejecuta tareas del grupo de subprocesos en lotes y, en última instancia, devuelve datos de resultado; siempre que este método obtenga el resultado de uno de los subprocesos, cancelará otros subprocesos en ejecución en el grupo de subprocesos;

3. invocarCualquiera()

Dentro de este método, todavía se llama al método doInvokeAny() para enviar un lote de subprocesos. Uno de ellos completa y devuelve el resultado, y los subprocesos restantes cancelan la operación;

4. invocarTodos()

El método invokeAll() implementa lógica con y sin configuración de tiempo de espera;

La lógica del método sin configuración de tiempo de espera es: encapsular las tareas por lotes enviadas en objetos RunnableFuture, luego llamar al método ejecutar () para ejecutar la tarea y agregar el Future resultante a la colección Future. Luego, se recorrerá la colección Future para determinar si la tarea es La ejecución está completa; si no se completa, se llamará al método get para bloquear hasta que se obtenga el resultado, y la excepción se ignorará en este momento; finalmente, finalmente, la identificación de finalización de todas las tareas es juzgado, y si no se cumple, se cancela la ejecución;

5. enviar()

Este método es relativamente simple. Encapsula la tarea en un objeto RunnableFuture. Después de llamar a ejecutar (), se devuelve el resultado futuro.

ProgramarServicioEjecutor

Esta interfaz hereda de ExecutorService y, además de heredar los métodos de la clase principal, también proporciona la función de procesamiento de tareas programadas.

Análisis en perspectiva del código fuente sobre cómo crear un grupo de subprocesos

​ Executors.newWorkStealingPool: este método es un nuevo método para crear un grupo de subprocesos en Java 8. Puede establecer el nivel paralelo para subprocesos y tiene mayor concurrencia y rendimiento; además, otros métodos para crear grupos de subprocesos, todos se denominan constructores. de ThreadPoolExecutor;

ThreadPoolExecutor crea un grupo de subprocesos

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
    
    
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

Al observar el código fuente de la clase ThreadPoolExecutor, encontramos que el hilo finalmente se construye llamando al método de construcción de excepción. Se han introducido otros parámetros de inicialización anteriormente;

La clase ForkJoinPool crea un grupo de subprocesos

public static ExecutorService newWorkStealingPool(int parallelism) {
    
    
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

    public static ExecutorService newWorkStealingPool() {
    
    
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

Como se puede ver en el código fuente anterior, el método Executors.newWorkStealingPool(), al construir el grupo de subprocesos, en realidad llama a ForkJoinPool para construir el grupo de subprocesos;

/**
     * Creates a {@code ForkJoinPool} with the given parameters, without
     * any security checks or parameter validation.  Invoked directly by
     * makeCommonPool.
     */
    private ForkJoinPool(int parallelism,
                         ForkJoinWorkerThreadFactory factory,
                         UncaughtExceptionHandler handler,
                         int mode,
                         String workerNamePrefix) {
    
    
        this.workerNamePrefix = workerNamePrefix;
        this.factory = factory;
        this.ueh = handler;
        this.config = (parallelism & SMASK) | mode;
        long np = (long)(-parallelism); // offset ctl counts
        this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
    }

Al observar el código fuente, aprendimos que los diversos métodos de construcción de ForkJoinPool finalmente llaman al método de construcción privado mencionado anteriormente;

Los parámetros de inicialización son los siguientes:

  • paralelismo: nivel de concurrencia
  • fábrica: fábrica para crear hilos
  • controlador: cuando el hilo en el hilo arroja una excepción no detectada, manéjela a través de este UncaughtExceptionHandler
  • modo: el valor indica FIFO_QUEUE o LIFO_QUEUE
  • trabajadorNamePrefix: prefijo de nombre de hilo para ejecutar tareas

ScheduledThreadPoolExecutor crea un grupo de subprocesos

    public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
    
    
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory, handler);
    }

ScheduledThreadPoolExecutor hereda de ThreadPoolExecutor. Al observar su código fuente, podemos ver que la esencia de su método de construcción es llamar al método de construcción de ThreadPoolExecutor, pero la cola pasa por DelayedWorkQueue.

Análisis del código fuente: cómo ejecutar ThreadPoolExecutor correctamente

Propiedades importantes en ThreadPoolExecutor

  • atributos relacionados con ctl

    La constante ctl de tipo AomaticInteger se utiliza durante todo el ciclo de vida del grupo de subprocesos.

    	//主要用来保存线程状态和线程数量,前3位保存线程状态,低29位报存线程数量
    	private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    	//线程池中线程数量(32-3)
        private static final int COUNT_BITS = Integer.SIZE - 3;
    	//线程池中的最大线程数量
        private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
    
        // runState is stored in the high-order bits
    	//线程池的运行状态
        private static final int RUNNING    = -1 << COUNT_BITS;
        private static final int SHUTDOWN   =  0 << COUNT_BITS;
        private static final int STOP       =  1 << COUNT_BITS;
        private static final int TIDYING    =  2 << COUNT_BITS;
        private static final int TERMINATED =  3 << COUNT_BITS;
    
        // Packing and unpacking ctl
    	//获取线程状态
        private static int runStateOf(int c)     {
          
           return c & ~CAPACITY; }
    	//获取线程数量
        private static int workerCountOf(int c)  {
          
           return c & CAPACITY; }
        private static int ctlOf(int rs, int wc) {
          
           return rs | wc; }
    
        /*
         * Bit field accessors that don't require unpacking ctl.
         * These depend on the bit layout and on workerCount being never negative.
         */
    
        private static boolean runStateLessThan(int c, int s) {
          
          
            return c < s;
        }
    
        private static boolean runStateAtLeast(int c, int s) {
          
          
            return c >= s;
        }
    
        private static boolean isRunning(int c) {
          
          
            return c < SHUTDOWN;
        }
    
        /**
         * Attempts to CAS-increment the workerCount field of ctl.
         */
        private boolean compareAndIncrementWorkerCount(int expect) {
          
          
            return ctl.compareAndSet(expect, expect + 1);
        }
    
        /**
         * Attempts to CAS-decrement the workerCount field of ctl.
         */
        private boolean compareAndDecrementWorkerCount(int expect) {
          
          
            return ctl.compareAndSet(expect, expect - 1);
        }
    
        /**
         * Decrements the workerCount field of ctl. This is called only on
         * abrupt termination of a thread (see processWorkerExit). Other
         * decrements are performed within getTask.
         */
        private void decrementWorkerCount() {
          
          
            do {
          
          } while (! compareAndDecrementWorkerCount(ctl.get()));
        }
    
  • Otros atributos importantes

    	//用于存放任务的阻塞队列
    	private final BlockingQueue<Runnable> workQueue;
    
    	//可重入锁
        private final ReentrantLock mainLock = new ReentrantLock();
    
        /**
         * Set containing all worker threads in pool. Accessed only when
         * holding mainLock.
         */
    	//存放线程池中线程的集合,访问这个集合时,必须先获得mainLock锁
        private final HashSet<Worker> workers = new HashSet<Worker>();
    
        /**
         * Wait condition to support awaitTermination
         */
    	//在锁内部阻塞等待条件完成
        private final Condition termination = mainLock.newCondition();
    
    	//线程工厂,以此来创建新线程
        private volatile ThreadFactory threadFactory;
    
        /**
         * Handler called when saturated or shutdown in execute.
         */
    	//拒绝策略
        private volatile RejectedExecutionHandler handler;
    	/**
         * The default rejected execution handler
         */
    	//默认的拒绝策略
        private static final RejectedExecutionHandler defaultHandler =
            new AbortPolicy();
    

Clases internas importantes en ThreadPoolExecutor

  • Obrero

    private final class Worker extends AbstractQueuedSynchronizer implements Runnable
        {
          
          
            /**
             * This class will never be serialized, but we provide a
             * serialVersionUID to suppress a javac warning.
             */
            private static final long serialVersionUID = 6138294804551838833L;
    
            /** Thread this worker is running in.  Null if factory fails. */
            final Thread thread;
            /** Initial task to run.  Possibly null. */
            Runnable firstTask;
            /** Per-thread task counter */
            volatile long completedTasks;
    
            /**
             * Creates with given first task and thread from ThreadFactory.
             * @param firstTask the first task (null if none)
             */
            Worker(Runnable firstTask) {
          
          
                setState(-1); // inhibit interrupts until runWorker
                this.firstTask = firstTask;
                this.thread = getThreadFactory().newThread(this);
            }
    
            /** Delegates main run loop to outer runWorker  */
            public void run() {
          
          
                runWorker(this);
            }
    
            // Lock methods
            //
            // The value 0 represents the unlocked state.
            // The value 1 represents the locked state.
    
            protected boolean isHeldExclusively() {
          
          
                return getState() != 0;
            }
    
            protected boolean tryAcquire(int unused) {
          
          
                if (compareAndSetState(0, 1)) {
          
          
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
                return false;
            }
    
            protected boolean tryRelease(int unused) {
          
          
                setExclusiveOwnerThread(null);
                setState(0);
                return true;
            }
    
            public void lock()        {
          
           acquire(1); }
            public boolean tryLock()  {
          
           return tryAcquire(1); }
            public void unlock()      {
          
           release(1); }
            public boolean isLocked() {
          
           return isHeldExclusively(); }
    
            void interruptIfStarted() {
          
          
                Thread t;
                if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
          
          
                    try {
          
          
                        t.interrupt();
                    } catch (SecurityException ignore) {
          
          
                    }
                }
            }
        }
    

    La clase Work implementa la interfaz Runnable y necesita reescribir el método run (). La esencia del método run del Worker es llamar al método runWorker () de ThreadPoolExecutor.

  • Denegar política

    En el grupo de subprocesos, si la cola WorkQueue está llena y no hay subprocesos inactivos, cuando se envía una nueva tarea, se ejecutará la política de rechazo;

    El grupo de subprocesos proporciona un total de cuatro estrategias de rechazo:

    • Lanzar una excepción directamente también es la política predeterminada; la clase de implementación es AbortPolicy
    • Utilice el hilo de llamada para realizar tareas; la clase de implementación es CallerRunsPolicy
    • Descartar la tarea más avanzada en la cola y ejecutar la tarea actual; DiscardOldestPolicy
    • Descarta directamente la tarea actual; la clase de implementación es DiscardPolicy

    En ThreadPoolExecutor, se proporcionan cuatro clases internas para implementar las estrategias correspondientes;

    public static class CallerRunsPolicy implements RejectedExecutionHandler {
          
          
            /**
             * Creates a {@code CallerRunsPolicy}.
             */
            public CallerRunsPolicy() {
          
           }
    
            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          
          
                if (!e.isShutdown()) {
          
          
                    r.run();
                }
            }
        }
    
        /**
         * A handler for rejected tasks that throws a
         * {@code RejectedExecutionException}.
         */
        public static class AbortPolicy implements RejectedExecutionHandler {
          
          
            /**
             * Creates an {@code AbortPolicy}.
             */
            public AbortPolicy() {
          
           }
    
            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          
          
                throw new RejectedExecutionException("Task " + r.toString() +
                                                     " rejected from " +
                                                     e.toString());
            }
        }
    
        /**
         * A handler for rejected tasks that silently discards the
         * rejected task.
         */
        public static class DiscardPolicy implements RejectedExecutionHandler {
          
          
            /**
             * Creates a {@code DiscardPolicy}.
             */
            public DiscardPolicy() {
          
           }
    
            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          
          
            }
        }
    
        /**
         * A handler for rejected tasks that discards the oldest unhandled
         * request and then retries {@code execute}, unless the executor
         * is shut down, in which case the task is discarded.
         */
        public static class DiscardOldestPolicy implements RejectedExecutionHandler {
          
          
            /**
             * Creates a {@code DiscardOldestPolicy} for the given executor.
             */
            public DiscardOldestPolicy() {
          
           }
    
            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          
          
                if (!e.isShutdown()) {
          
          
                    e.getQueue().poll();
                    e.execute(r);
                }
            }
        }
    

    También podemos personalizar la política de rechazo implementando la interfaz RejectedExecutionHandler y anulando el método rechazadoExecution();

    Al crear un hilo, pase nuestra política de rechazo personalizada a través del constructor de ThreadPoolExecutor;

Análisis del código fuente Proceso central ThreadPoolExecutor

Hay una colección de subprocesos de trabajo en ThreadPoolExecutor. Los usuarios pueden agregar tareas al grupo de subprocesos. Los subprocesos en la colección de trabajadores pueden ejecutar tareas directamente u obtener tareas de la cola de tareas y ejecutarlas;

ThreadPoolExecutor proporciona todo el proceso del grupo de subprocesos desde la creación, la ejecución de la tarea hasta la muerte;

En ThreadPoolExecutor, la lógica del grupo de subprocesos se refleja principalmente en ejecutar (comando ejecutable), addWorker (primera tarea ejecutable, núcleo booleano), addWorkerFailed (Trabajador w) y otros métodos y estrategias de rechazo; a continuación, analizaremos estos métodos centrales en profundidad. .

ejecutar (comando ejecutable)

La función de este método es enviar tareas de tipo Runnable al grupo de subprocesos;

public void execute(Runnable command) {
    
    
        if (command == null)
            //若提交的任务为空,则提交空指针异常
            throw new NullPointerException();
       //获取线程池的状态,和线程池中线程数量
        int c = ctl.get();
    	//若线程池中线程数小于核心线程数
        if (workerCountOf(c) < corePoolSize) {
    
    
            //重新开启线程执行任务
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
    	//若线程池处于RUNNING状态,则将任务添加到阻塞队列中
    	//只有线程池处于RUNNING状态时,才能添加到队列
        if (isRunning(c) && workQueue.offer(command)) {
    
    
            //再次获取线程次状态和线程池数量,用于二次检查
            //向队列中添加线程成功,但由于其他线程可能会修改线程池状态,所以这里需要进行二次检查
            int recheck = ctl.get();
            //如果线程池没有再处于RUNNING状态,则从队列中删除任务
            if (! isRunning(recheck) && remove(command))
                //执行拒绝策略
                reject(command);
            else if (workerCountOf(recheck) == 0)
                //若线程池为空,则新建一个线程加入
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            //任务队列已满,则新建一个Worker线程,若新增失败,则执行拒绝策略
            reject(command);
    }

addWorker (primera tarea ejecutable, núcleo booleano)

Este método generalmente se puede dividir en tres partes: la primera parte trata principalmente sobre CAS para agregar de forma segura subprocesos de trabajo al grupo de subprocesos, la segunda parte trata sobre agregar nuevos subprocesos de trabajo y la tercera parte trata sobre agregar subprocesos a los trabajadores mediante concurrencia segura. e iniciar el hilo para realizar la tarea

private boolean addWorker(Runnable firstTask, boolean core) {
    
    
    //循环标签,重试的标识
    retry:
    for (;;) {
    
    
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        //检查队列是否在某些特定条件下为空
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
		//此循环中主要是通过CAS的方式增加线程个数
        for (;;) {
    
    
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            //CAS的方式增加线程数量
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
    //跳出最外层循环,说明已通过CAS增加线程成功
    //此时创建新的线程
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
    
    
        //将新建线程封装成Woker
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
    
    
            //独占锁,保证操作workers的同步
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
    
    
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
    
    
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    //将Worker加入队列
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
    
    
                //释放独占锁
                mainLock.unlock();
            }
            if (workerAdded) {
    
    
                t.start();
                workerStarted = true;
            }
        }
    } finally {
    
    
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

addWorkerFailed(Trabajador w)

En el método addWorker(Runnable firstTask, boolean core), si falla al agregar el subproceso de trabajo o si el subproceso de trabajo no se inicia, se llama al método addWorkerFailed(Worker w); este método es más simple, obtenga el bloqueo exclusivo y elimínelo. de los trabajadores tareas, y reducir el número de tareas en una a través de CAS, y finalmente liberar el bloqueo;

private void addWorkerFailed(Worker w) {
    
    
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
        if (w != null)
            workers.remove(w);
        decrementWorkerCount();
        tryTerminate();
    } finally {
    
    
        mainLock.unlock();
    }
}
  • Denegar política

    /**
     * Invokes the rejected execution handler for the given command.
     * Package-protected for use by ScheduledThreadPoolExecutor.
     */
    final void reject(Runnable command) {
          
          
        handler.rejectedExecution(command, this);
    }
    

    Las cuatro clases de implementación de RejectExecutionHandler son exactamente las clases de implementación de las cuatro estrategias de rechazo proporcionadas por el hilo;

    La estrategia específica de este método se determina en función de los parámetros pasados ​​al crear el grupo de subprocesos; de forma predeterminada, se utiliza la estrategia de rechazo predeterminada;

Análisis del código fuente del proceso de ejecución de Woker en el grupo de subprocesos

Análisis de la clase trabajadora

Desde el punto de vista de la estructura de clases, Woker hereda la clase AQS (AbstractQueueSynchronizer) e implementa la interfaz Runnable; esencialmente la clase Woker es un componente de sincronización y un hilo que realiza tareas;

private final class Worker extends AbstractQueuedSynchronizer implements Runnable
{
    
    
    /**
     * This class will never be serialized, but we provide a
     * serialVersionUID to suppress a javac warning.
     */
    private static final long serialVersionUID = 6138294804551838833L;

    /** Thread this worker is running in.  Null if factory fails. */
    final Thread thread;
    /** Initial task to run.  Possibly null. */
    Runnable firstTask;
    /** Per-thread task counter */
    volatile long completedTasks;

    /**
     * Creates with given first task and thread from ThreadFactory.
     * @param firstTask the first task (null if none)
     */
    Worker(Runnable firstTask) {
    
    
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    /** Delegates main run loop to outer runWorker  */
    public void run() {
    
    
        runWorker(this);
    }

    // Lock methods
    //
    // The value 0 represents the unlocked state.
    // The value 1 represents the locked state.

    protected boolean isHeldExclusively() {
    
    
        return getState() != 0;
    }

    protected boolean tryAcquire(int unused) {
    
    
        if (compareAndSetState(0, 1)) {
    
    
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    protected boolean tryRelease(int unused) {
    
    
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    public void lock()        {
    
     acquire(1); }
    public boolean tryLock()  {
    
     return tryAcquire(1); }
    public void unlock()      {
    
     release(1); }
    public boolean isLocked() {
    
     return isHeldExclusively(); }

    void interruptIfStarted() {
    
    
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
    
    
            try {
    
    
                t.interrupt();
            } catch (SecurityException ignore) {
    
    
            }
        }
    }
}

Como se puede ver en el constructor de la clase Worker, el estado de sincronización primero se establece en -1, esto es para evitar que el método runWorker se interrumpa antes de ejecutarse;

Esto se debe a que si otros subprocesos llaman al método ShutdownNow () en el grupo de subprocesos, si el estado en la clase de trabajador es> 0, el subproceso se interrumpirá, y si el estado es -1, el subproceso no se interrumpirá;

La clase Worker implementa la interfaz Runnable y necesita anular el método de ejecución, que en realidad llama al método runWorker () de ThreadPoolExecutor;

ejecutarTrabajador(Trabajador w)

final void runWorker(Worker w) {
    
    
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    //释放锁,将state设置为0,允许中断
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
    
    
       //若任务不为空,或队列中获取的任务不为空,则进入循环
        while (task != null || (task = getTask()) != null) {
    
    
            //任务不为空,则先获取woker线程的独占锁
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            //若线程次已经停止,线程中断时未中断成功
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                //执行中断操作
                wt.interrupt();
            try {
    
    
                //任务执行前置逻辑
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
    
    
                    //任务执行
                    task.run();
                } catch (RuntimeException x) {
    
    
                    thrown = x; throw x;
                } catch (Error x) {
    
    
                    thrown = x; throw x;
                } catch (Throwable x) {
    
    
                    thrown = x; throw new Error(x);
                } finally {
    
    
                    //任务执行后置逻辑
                    afterExecute(task, thrown);
                }
            } finally {
    
    
                //任务执行完成后,将其置空
                task = null;
                //已完成任务数加一
                w.completedTasks++;
                //释放锁
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
    
    
        //执行Worker完成退出逻辑
        processWorkerExit(w, completedAbruptly);
    }
}

Del análisis del código fuente anterior, podemos saber que cuando la tarea obtenida por Woker del hilo está vacía, se llamará al método getTask () para obtener la tarea de la cola;

obtener tarea()

private Runnable getTask() {
    
    
    boolean timedOut = false; // Did the last poll() time out?
    //自旋
    for (;;) {
    
    
        int c = ctl.get();
        //获取线程池状态
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        //检测队列在线程池关闭或停止时,是否为空
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
    
    
            //减少Worker线程数量
            decrementWorkerCount();
            return null;
        }
        //获取线程池中线程数量
        int wc = workerCountOf(c);

        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
    
    
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
    
    
            //从任务队列中获取任务
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                //任务不为空,直接返回
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
    
    
            timedOut = false;
        }
    }
}
  • beforeExecute(Subproceso t, Ejecutable r)

    protected void beforeExecute(Thread t, Runnable r) {
          
           }
    

    El cuerpo de este método está vacío, lo que significa que puede crear una subclase de ThreadPoolExecutor para anular este método, de modo que nuestra lógica previa personalizada pueda ejecutarse antes de que el grupo de subprocesos realmente ejecute la tarea;

  • afterExecute(Runnable r, Throwable t)

    protected void afterExecute(Runnable r, Throwable t) {
          
           }
    

    Igual que arriba, podemos anular este método en una subclase, de modo que nuestra lógica de posprocesamiento personalizada pueda ejecutarse después de que el grupo de subprocesos ejecute la tarea.

  • ProcessWorkerExit(Trabajador w, booleano completado abruptamente)

    La lógica principal de este método es ejecutar la lógica de salir del subproceso de trabajo y realizar algún trabajo de limpieza;

  • intentarTerminar()

    final void tryTerminate() {
          
          
        //自旋
        for (;;) {
          
          
            //获取ctl
            int c = ctl.get();
            if (isRunning(c) ||
                runStateAtLeast(c, TIDYING) ||
                (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
                return;
            if (workerCountOf(c) != 0) {
          
           // Eligible to terminate
                //若当前线程池中线程数量不为0,则中断线程
                interruptIdleWorkers(ONLY_ONE);
                return;
            }
            //获取线程池的全局锁
            final ReentrantLock mainLock = this.mainLock;
            //加锁
            mainLock.lock();
            try {
          
          
                if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
          
          
                    try {
          
          
                        terminated();
                    } finally {
          
          
                        //将线程状态设置为TERMINATED
                        ctl.set(ctlOf(TERMINATED, 0));
                        //唤醒所有因调用awaitTermination()而阻塞的线程
                        termination.signalAll();
                    }
                    return;
                }
            } finally {
          
          
                //释放锁
                mainLock.unlock();
            }
            // else retry on failed CAS
        }
    }
    
  • Terminar()

    protected void terminated() {
          
           }
    

    El cuerpo de este método está vacío, podemos anular este método en su subclase, de modo que en el método tryTerminate() se pueda ejecutar nuestro método personalizado;

¿Cómo logra el grupo de subprocesos de análisis del código fuente una salida elegante?

cerrar()

Cuando se utiliza un grupo de subprocesos, cuando se llama al método de apagado (), el grupo de subprocesos ya no aceptará tareas recién enviadas y los subprocesos que ya se están ejecutando continuarán ejecutándose;

Este método es un método sin bloqueo, regresará inmediatamente después de ser llamado y no esperará a que se completen todas las tareas del hilo antes de regresar;

public void shutdown() {
    
    
    //获取线程池的全局锁
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
        //检查是否有关闭线程池的权限
        checkShutdownAccess();
        //将当前线程池的状态设置为SHUTDOWN
        advanceRunState(SHUTDOWN);
        //中断woker线程
        interruptIdleWorkers();
        //调用ScheduledThreadPoolExecutor的钩子函数
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
    
    
        //释放锁
        mainLock.unlock();
    }
    tryTerminate();
}

apagar ahora()

Si se llama al método ShutdownNow() del grupo de subprocesos, el subproceso ya no recibirá las tareas recién enviadas, los subprocesos en la cola workQueue también se descartarán y los subprocesos en ejecución se interrumpirán; el método regresará inmediatamente y el el resultado devuelto es la tarea Lista de tareas descartadas en la cola workQueue

public List<Runnable> shutdownNow() {
    
    
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        //清空、丢弃队列中任务
        tasks = drainQueue();
    } finally {
    
    
        mainLock.unlock();
    }
    tryTerminate();
    //返回任务列表
    return tasks;
}

awaitTermination (tiempo de espera prolongado, unidad TimeUnit)

Cuando el grupo de subprocesos llama a awaitTermination, bloqueará el subproceso de la persona que llama y no regresará hasta que el estado del grupo de subprocesos cambie a TERNINADO o se alcance el período de tiempo de espera.

public boolean awaitTermination(long timeout, TimeUnit unit)
    throws InterruptedException {
    
    
    long nanos = unit.toNanos(timeout);
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
        for (;;) {
    
    
            if (runStateAtLeast(ctl.get(), TERMINATED))
                return true;
            if (nanos <= 0)
                return false;
            nanos = termination.awaitNanos(nanos);
        }
    } finally {
    
    
        //释放锁
        mainLock.unlock();
    }
}

La lógica general de este método es: primero obtenga el bloqueo exclusivo del subproceso de trabajo, luego gire y determine que el estado del grupo de subprocesos cambia a TERMINADO; si es así, devuelva verdadero; de lo contrario, verifique si se agota el tiempo de espera, si se agota el tiempo de espera, devuelve falso; si no se agota el tiempo de espera, reinicie Establezca el período de tiempo de espera restante;

Clases clave en AQS

cuenta atrás

  • Descripción general

Clase auxiliar de sincronización, que puede bloquear la ejecución del hilo actual. Es decir, uno o más subprocesos pueden esperar hasta que otros subprocesos terminen de ejecutarse. Utilice un contador dado para la inicialización. La operación del contador es una operación atómica, es decir, solo un subproceso puede operar el contador al mismo tiempo.

El subproceso que llama al método await() de la clase modificada esperará hasta que otros subprocesos llamen al método countDown() de la clase y establezcan el valor del contador actual en 0;

Cada vez que se llama al método countDown() de esta clase, el valor del contador disminuirá en uno;

Cuando el valor del contador disminuye a 0, todos los subprocesos bloqueados y en espera llamando al método await() continuarán ejecutándose; esta operación solo puede ocurrir una vez, porque el valor del contador no se puede restablecer;

Si necesita una versión que restablezca el recuento, considere usar CyclicBarrier.

CountDownLatch admite esperar un período de tiempo determinado y no espera más que el tiempo dado; cuando lo usa, solo necesita pasar el tiempo dado en el método await();

public int await(long timeout, TimeUnit unit)
    throws InterruptedException,
           BrokenBarrierException,
           TimeoutException {
    
    
    return dowait(true, unit.toNanos(timeout));
}
  • escenas a utilizar

    En algunos escenarios, el programa necesita esperar a que se completen una o más condiciones antes de continuar realizando operaciones posteriores. Una aplicación típica es la computación paralela: cuando se procesa una tarea con una gran cantidad de cálculos, se puede dividir en varias tareas pequeñas. Después de esperar a que se completen todas las subtareas, la tarea principal obtiene los resultados de todas las subtareas. y los resume.

  • ejemplo de código

    Llamar al método Shutdown() de ExecutorService no destruirá todos los subprocesos inmediatamente, pero permitirá que se ejecuten todos los subprocesos existentes.

    Luego destruye el grupo de subprocesos.

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    @Slf4j
    public class CountDownLatchExample {
          
          
        private static final int threadCount = 200;
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService exec = Executors.newCachedThreadPool();
            final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
            for (int i = 0; i < threadCount; i++) {
          
          
                final int threadNum = i;
                exec.execute(() -> {
          
          
                    try {
          
          
                        test(threadNum);
                    } catch (InterruptedException e) {
          
          
                        e.printStackTrace();
                    } finally {
          
          
                        countDownLatch.countDown();
                    }
                });
            }
            countDownLatch.await();
            log.info("finish");
            exec.shutdown();
        }
    
        private static void test(int threadNum) throws InterruptedException {
          
          
            Thread.sleep(100);
            log.info("{}", threadNum);
        }
    }
    

    El código que admite esperar un tiempo determinado es el siguiente:

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    @Slf4j
    public class CountDownLatchExample {
          
          
        private static final int threadCount = 200;
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService exec = Executors.newCachedThreadPool();
            final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
            for (int i = 0; i < threadCount; i++) {
          
          
                final int threadNum = i;
                exec.execute(() -> {
          
          
                    try {
          
          
                        test(threadNum);
                    } catch (InterruptedException e) {
          
          
                        e.printStackTrace();
                    } finally {
          
          
                        countDownLatch.countDown();
                    }
                });
            }
            countDownLatch.await(10, TimeUnit.MICROSECONDS);
            log.info("finish");
            exec.shutdown();
        }
    
        private static void test(int threadNum) throws InterruptedException {
          
          
            Thread.sleep(100);
            log.info("{}", threadNum);
        }
    }
    

Semáforo

  • Descripción general

    Controle la cantidad de subprocesos simultáneos al mismo tiempo. Puede controlar el semáforo y controlar la cantidad de subprocesos que acceden a un determinado recurso al mismo tiempo.

    Proporciona dos métodos principales: adquirir() y liberar()

    adquirir () significa obtener un permiso de acceso, y si no se obtiene, se bloqueará y esperará; liberar () liberará un permiso una vez completado.

    Semaphore mantiene el número actualmente accesible; utiliza un mecanismo de sincronización para controlar el número al que se puede acceder simultáneamente.

    Semaphore puede implementar listas enlazadas de tamaño limitado

  • escenas a utilizar

    El semáforo se utiliza a menudo para recursos con acceso limitado.

  • ejemplo de código

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    public class SemaphoreExample {
          
          
        private static final int threadCount = 200;
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService exec = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(3);
            for (int i = 0; i < threadCount; i++) {
          
          
                final int threadNum = i;
                exec.execute(() -> {
          
          
                    try {
          
          
                        semaphore.acquire(); //获取一个许可
                        test(threadNum);
                        semaphore.release(); //释放一个许可
                    } catch (InterruptedException e) {
          
          
                        e.printStackTrace();
                    }
                });
            }
            exec.shutdown();
        }
    
        private static void test(int threadNum) throws InterruptedException {
          
          
            log.info("{}", threadNum);
            Thread.sleep(1000);
        }
    }
    

    Adquiera y libere varias licencias a la vez

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    public class SemaphoreExample {
          
          
        private static final int threadCount = 200;
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService exec = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(3);
            for (int i = 0; i < threadCount; i++) {
          
          
                final int threadNum = i;
                exec.execute(() -> {
          
          
                    try {
          
          
                        semaphore.acquire(3); //获取多个许可
                        test(threadNum);
                        semaphore.release(3); //释放多个许可
                    } catch (InterruptedException e) {
          
          
                        e.printStackTrace();
                    }
                });
            }
            log.info("finish");
            exec.shutdown();
        }
    
        private static void test(int threadNum) throws InterruptedException {
          
          
            log.info("{}", threadNum);
            Thread.sleep(1000);
        }
    }
    

    Supongamos que existe tal escenario. Supongamos que el número máximo de concurrencias permitidas actualmente por el sistema es 3. Si excede 3, debe descartarse. Este escenario también se puede implementar a través de Semaphore:

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    public class SemaphoreExample {
          
          
        private static final int threadCount = 200;
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService exec = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(3);
            for (int i = 0; i < threadCount; i++) {
          
          
                final int threadNum = i;
                exec.execute(() -> {
          
          
                    try {
          
          
                        //尝试获取一个许可,也可以尝试获取多个许可,
                        //支持尝试获取许可超时设置,超时后不再等待后续线程的执行
                        //具体可以参见Semaphore的源码
                        if (semaphore.tryAcquire()) {
          
          
                            test(threadNum);
                            semaphore.release(); //释放一个许可
                        }
                    } catch (InterruptedException e) {
          
          
                        e.printStackTrace();
                    }
                });
            }
            log.info("finish");
            exec.shutdown();
        }
    
        private static void test(int threadNum) throws InterruptedException {
          
          
            log.info("{}", threadNum);
            Thread.sleep(1000);
        }
    }
    

Barrera cíclica

  • Descripción general

    Es una clase auxiliar de sincronización que permite que un grupo de subprocesos se esperen entre sí hasta llegar a un punto de barrera común, a través de ella, varios subprocesos pueden esperarse entre sí y solo cuando cada subproceso esté listo, cada subproceso podrá continuar ejecutándose. .

    Similar a countDownLatch, todos se implementan mediante contadores. Cuando un subproceso llama al método await() de CyclicBarrier, ingresa al estado de espera y el contador realiza una operación de incremento; cuando el valor del contador aumenta al valor inicial establecido, Todos los subprocesos que entran en estado de espera debido al método await() se despertarán y continuarán realizando sus operaciones posteriores. CyclicBarrier se puede reutilizar después de liberar el hilo en espera, por lo que CyclicBarrier también se denomina barrera cíclica.

  • escenas a utilizar

    Se puede utilizar en escenarios donde varios subprocesos calculan datos y finalmente fusionan los resultados del cálculo.

  • La diferencia entre countDownLatch y countDownLatch

    1. El contador de countDownLatch solo se puede usar una vez, mientras que el contador de CyclicBarrier se puede restablecer usando reSet() y usarse cíclicamente.
    2. Lo que implementa countDownLatch es que 1 o n subprocesos esperan a que otros subprocesos se completen antes de poder continuar ejecutándose. Describe la relación entre 1 o más subprocesos que esperan a otros subprocesos, y CyclicBarrier significa principalmente que los subprocesos en un grupo de subprocesos se esperan entre sí. Cada hilo cumple con las condiciones comunes antes de continuar ejecutándose, lo que describe la relación de espera interna de múltiples hilos.
    3. CyclicBarrier puede manejar escenarios más complejos: cuando ocurre un error de cálculo, el contador se puede restablecer y el hilo se puede ejecutar nuevamente.
    4. CyclicBarrier proporciona métodos más útiles, como getNumberByWaiting() para obtener el número de subprocesos en espera y el método isBroken() para determinar si un subproceso está interrumpido.
  • ejemplo de código

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CyclicBarrier;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    @Slf4j
    public class CyclicBarrierExample {
          
          
        private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
    
        public static void main(String[] args) throws Exception {
          
          
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < 10; i++) {
          
          
                final int threadNum = i;
                Thread.sleep(1000);
                executorService.execute(() -> {
          
          
                    try {
          
          
                        race(threadNum);
                    } catch (Exception e) {
          
          
                        e.printStackTrace();
                    }
                });
            }
            executorService.shutdown();
        }
    
        private static void race(int threadNum) throws Exception {
          
          
            Thread.sleep(1000);
            log.info("{} is ready", threadNum);
            cyclicBarrier.await();
            log.info("{} continue", threadNum);
        }
    }
    

    El código de muestra para configurar el tiempo de espera es el siguiente:

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.*;
    
    @Slf4j
    public class CyclicBarrierExample {
          
          
        private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
    
        public static void main(String[] args) throws Exception {
          
          
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < 10; i++) {
          
          
                final int threadNum = i;
                Thread.sleep(1000);
                executorService.execute(() -> {
          
          
                    try {
          
          
                        race(threadNum);
                    } catch (Exception e) {
          
          
                        e.printStackTrace();
                    }
                });
            }
            executorService.shutdown();
        }
    
        private static void race(int threadNum) throws Exception {
          
          
            Thread.sleep(1000);
            log.info("{} is ready", threadNum);
            try {
          
          
                cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
            } catch (BrokenBarrierException | TimeoutException e) {
          
          
                log.warn("BarrierException", e);
            }
            log.info("{} continue", threadNum);
        }
    }
    

    Al declarar CyclicBarrier, también puede especificar un Runnable. Cuando el hilo alcanza la barrera, el método Runnable se puede ejecutar primero. El código de muestra es el siguiente:

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CyclicBarrier;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    @Slf4j
    public class CyclicBarrierExample {
          
          
        private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
          
          
            log.info("callback is running");
        });
    
        public static void main(String[] args) throws Exception {
          
          
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < 10; i++) {
          
          
                final int threadNum = i;
                Thread.sleep(1000);
                executorService.execute(() -> {
          
          
                    try {
          
          
                        race(threadNum);
                    } catch (Exception e) {
          
          
                        e.printStackTrace();
                    }
                });
            }
            executorService.shutdown();
        }
    
        private static void race(int threadNum) throws Exception {
          
          
            Thread.sleep(1000);
            log.info("{} is ready", threadNum);
            cyclicBarrier.await();
            log.info("{} continue", threadNum);
        }
    }
    

Bloqueo de teclas en AQS

Bloqueo reentrante

  • Descripción general

    Los bloqueos proporcionados en Java se dividen principalmente en dos categorías: uno es el bloqueo modificado sincronizado, el otro es el bloqueo proporcionado en JUC y el bloqueo central en JUC es ReentrantLock.

    La diferencia entre ReentrantLock y Sincronizado:

    1. Reentrada

      Cuando el mismo hilo ingresa a ambos una vez, el contador de bloqueo se incrementará en 1. Cuando el contador de bloqueo caiga a 0, el bloqueo se liberará.

    2. Implementación de bloqueo

      Sincronizado se implementa en base a JVM; ReentrantLock se implementa en base a JDK

    3. Diferencia de rendimiento

      Antes de la optimización sincronizada, el rendimiento era mucho peor que ReentrantLock, pero después de JDK6, después de que Synchronized introdujo bloqueos sesgados y bloqueos livianos (es decir, bloqueos giratorios), el rendimiento era casi el mismo.

    4. Diferencia funcional

      Conveniencia:

      Sincronizado es más conveniente de usar, y el compilador bloquea y libera el bloqueo; ReentrantLock requiere bloqueo y liberación manual, y es mejor liberar el bloqueo finalmente

      Flexibilidad y granularidad:

      ReentrantLock es mejor que sincronizado aquí

    Características exclusivas de ReentrantLock:

    1. ReentrantLock puede especificar un bloqueo justo o un bloqueo injusto. Sincronizado solo puede usar bloqueos injustos. El bloqueo justo significa que el hilo que espera primero obtiene el bloqueo primero.
    2. Proporciona una clase de condición que puede activar subprocesos que deben activarse en grupos. Sincronizado solo puede activar un subproceso aleatoriamente o activar todos los subprocesos.
    3. Proporciona un mecanismo para interrumpir subprocesos que esperan bloqueos, lock.lockInterruptily(). La implementación de ReentrantLock es un bloqueo giratorio que implementa el bloqueo llamando a la operación CAS. El rendimiento es mejor porque evita que el estado de bloqueo del hilo entre en el estado del núcleo.
    4. En general, ReentrantLock puede hacer todo lo que Synchronized puede hacer. En términos de rendimiento, ReentrantLock es mejor que Synchronized

    Ventajas sincronizadas:

    1. No es necesario liberar el bloqueo manualmente, la JVM lo maneja automáticamente. Si ocurre una excepción, la JVM liberará el bloqueo automáticamente.
    2. Cuando la JVM realiza solicitudes y liberaciones de administración de bloqueos, la JVM puede generar volcados de subprocesos con información de bloqueo. Esta información es muy útil para la depuración porque puede identificar fuentes de interbloqueos y otros comportamientos anormales. ReentrantLock es solo una clase ordinaria y la JVM no sabe qué hilo posee el bloqueo.
    3. Synchronized se puede utilizar en todas las versiones de JVM. Es posible que ReentrantLock no sea compatible con algunas JVM anteriores a la versión 1.5.

    Descripción de algunos métodos en ReentrantLock:

    1. boolean tryLock(): Adquiere el bloqueo solo si el bloqueo no está retenido por otro subproceso en el momento de la llamada.
    2. tryLock booleano (tiempo de espera prolongado, unidad TimeUnit): adquiere este bloqueo si otro subproceso no lo mantiene en el momento dado y el subproceso actual no se interrumpe.
    3. void lockInterruptfully(): si el hilo actual no se interrumpe, adquiere el bloqueo; si se interrumpe, lanza una excepción
    4. boolean isLocked(): consulta si este bloqueo está retenido por algún subproceso
    5. boolean isHeldByCurrentThread(): consulta si el hilo actual permanece bloqueado
    6. boolean isFair(): determina si es un bloqueo justo
    7. boolean hasQueuedThread (subproceso de subproceso): consulta si el subproceso especificado está esperando adquirir este bloqueo
    8. boolean hasQueuedThreads(): ¿Hay subprocesos esperando adquirir este bloqueo?
    9. int getHoldCount(): consulta el número de bloqueos retenidos por el hilo actual
  • ejemplo de código

    package io.binghe.concurrency.example.lock;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    @Slf4j
    public class LockExample {
          
          
        //请求总数
        public static int clientTotal = 5000;
        //同时并发执行的线程数
        public static int threadTotal = 200;
        public static int count = 0;
        private static final Lock lock = new ReentrantLock();
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal; i++) {
          
          
                executorService.execute(() -> {
          
          
                    try {
          
          
                        semaphore.acquire();
                        add();
                        semaphore.release();
                    } catch (Exception e) {
          
          
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("count:{}", count);
        }
    
        private static void add() {
          
          
            lock.lock();
            try {
          
          
                count++;
            } finally {
          
          
                lock.unlock();
            }
        }
    }
    

Bloqueo de lectura y escritura reentrante

  • Descripción general

    Los bloqueos de lectura y escritura solo pueden adquirir bloqueos de escritura cuando no hay bloqueos de lectura; si no se puede adquirir el bloqueo de escritura, provocará la inanición del bloqueo de escritura;

    En escenarios donde hay más lectura y menos escritura, el rendimiento de ReentrantReadWriteLock es mucho mayor que el de ReentrantLock. No se afectan entre sí durante la lectura de subprocesos múltiples. A diferencia de ReentrantLock, incluso la lectura de subprocesos múltiples requiere que cada subproceso adquiera una lectura. bloqueo; sin embargo, cuando cualquier hilo está escribiendo, similar a ReentrantLock, sin importar si otros hilos están leyendo o escribiendo, deben adquirir el bloqueo de escritura. Cabe señalar que el mismo hilo puede contener bloqueos de lectura y bloqueos de escritura al mismo tiempo.

  • ejemplo de código

    package io.binghe.concurrency.example.lock;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Map;
    import java.util.Set;
    import java.util.TreeMap;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    @Slf4j
    public class LockExample {
          
          
        private final Map<String, Data> map = new TreeMap<>();
        private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        private final Lock readLock = lock.readLock();
        private final Lock writeLock = lock.writeLock();
    
        public Data get(String key) {
          
          
            readLock.lock();
            try {
          
          
                return map.get(key);
            } finally {
          
          
                readLock.unlock();
            }
        }
    
        public Set<String> getAllKeys() {
          
          
            readLock.lock();
            try {
          
          
                return map.keySet();
            } finally {
          
          
                readLock.unlock();
            }
        }
    
        public Data put(String key, Data value) {
          
          
            writeLock.lock();
            try {
          
          
                return map.put(key, value);
            } finally {
          
          
                writeLock.unlock();
            }
        }
    
        class Data {
          
          
    
        }
    }
    

Bloqueo estampado

  • Descripción general

StampedLock es la implementación de ReentrantReadWriteLock. La principal diferencia es que StampedLock no permite la reentrada. Con la adición de la función de lectura optimista, será más complicado de usar, pero tendrá mejor rendimiento.

StampedLock controla tres modos de bloqueo: lectura, escritura, lectura optimista

El estado de StampedLock consta de dos partes: versión y modo. El método de adquisición del bloqueo devuelve un número llamado ticket, que utiliza el estado del bloqueo correspondiente para representar y controlar el acceso relacionado. El número 0 indica que el bloqueo de escritura no está autorizado para acceder. .

Los bloqueos de lectura se dividen en bloqueos pesimistas y bloqueos optimistas. Las lecturas optimistas se dan en escenarios donde hay más lectura y menos escritura. Los optimistas creen que la probabilidad de que se escriba y lea al mismo tiempo es muy pequeña, por lo tanto, los bloqueos optimistas están completamente bloqueados. con bloqueos de lectura. El programa puede verificar si se han realizado cambios escribiendo después de leer y luego tomar medidas posteriores, lo que puede mejorar en gran medida el rendimiento del programa.

En resumen, en escenarios donde hay cada vez más hilos de lectura, StampedLock mejora enormemente el rendimiento del programa.

import java.util.concurrent.locks.StampedLock;

class Point {
    
    
    private double x, y;
    private final StampedLock sl = new StampedLock();

    void move(double deltaX, double deltaY) {
    
     // an exclusively locked method
        long stamp = sl.writeLock();
        try {
    
    
            x += deltaX;
            y += deltaY;
        } finally {
    
    
            sl.unlockWrite(stamp);
        }
    }

    //下面看看乐观读锁案例
    double distanceFromOrigin() {
    
     // A read-only method
        long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
        double currentX = x, currentY = y; //将两个字段读入本地局部变量
        if (!sl.validate(stamp)) {
    
     //检查发出乐观读锁后同时是否有其他写锁发生?
            stamp = sl.readLock(); //如果没有,我们再次获得一个读悲观锁
            try {
    
    
                currentX = x; // 将两个字段读入本地局部变量
                currentY = y; // 将两个字段读入本地局部变量
            } finally {
    
    
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    //下面是悲观读锁案例
    void moveIfAtOrigin(double newX, double newY) {
    
     // upgrade
	// Could instead start with optimistic, not read mode
        long stamp = sl.readLock();
        try {
    
    
            while (x == 0.0 && y == 0.0) {
    
     //循环,检查当前状态是否符合
                long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
                if (ws != 0L) {
    
     //这是确认转为写锁是否成功
                    stamp = ws; //如果成功 替换票据
                    x = newX; //进行状态改变
                    y = newY; //进行状态改变
                    break;
                } else {
    
     //如果不能成功转换为写锁
                    sl.unlockRead(stamp); //我们显式释放读锁
                    stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
                }
            }
        } finally {
    
    
            sl.unlock(stamp); //释放读锁或写锁
        }
    }
}
  • ejemplo de código

    package io.binghe.concurrency.example.lock;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.locks.StampedLock;
    
    @Slf4j
    public class LockExample {
          
          
        //请求总数
        public static int clientTotal = 5000;
        //同时并发执行的线程数
        public static int threadTotal = 200;
        public static int count = 0;
        private static final StampedLock lock = new StampedLock();
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal; i++) {
          
          
                executorService.execute(() -> {
          
          
                    try {
          
          
                        semaphore.acquire();
                        add();
                        semaphore.release();
                    } catch (Exception e) {
          
          
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("count:{}", count);
        }
    
        private static void add() {
          
          
    //加锁时返回一个long类型的票据
            long stamp = lock.writeLock();
            try {
          
          
                count++;
            } finally {
          
          
    //释放锁的时候带上加锁时返回的票据
                lock.unlock(stamp);
            }
        }
    }
    

    Podemos hacer un juicio preliminar sobre si elegir Synchronized o ReentrantLock de esta manera:

    1. Sincronizado es una buena implementación de bloqueo de uso general cuando solo hay unos pocos competidores.
    2. Hay muchos competidores, pero la tendencia de crecimiento de los subprocesos es predecible. En este momento, usar ReentrantLock es una buena implementación de bloqueo general.
    3. Sincronizado no causará un punto muerto. El uso inadecuado de otras cerraduras puede causar un punto muerto.

Condición

  • Descripción general

    La condición es una clase de herramienta para coordinar la comunicación entre múltiples subprocesos. Su uso puede proporcionar una mayor flexibilidad. Por ejemplo, puede realizar la función de notificación multicanal, es decir, en un objeto Lock, se pueden crear múltiples instancias de Condición y el subproceso El objeto se puede registrar en una condición específica, para realizar de forma selectiva la notificación de subprocesos y ser más flexible en la programación de subprocesos.

  • Características

    1. La premisa de Condition es Lock, y el objeto Condition se crea mediante el método newCondition () en AQS.
    2. El método await() de Condición indica que el hilo se elimina de AQS y libera el bloqueo adquirido por el hilo; y ingresa a la cola de espera de Condición, esperando ser notificado.
    3. El método signal() de Condición significa despertar los nodos en la cola de espera de Condición y prepararse para adquirir el bloqueo.
  • ejemplo de código

    package io.binghe.concurrency.example.lock;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;
    
    @Slf4j
    public class LockExample {
          
          
        public static void main(String[] args) {
          
          
            ReentrantLock reentrantLock = new ReentrantLock();
            Condition condition = reentrantLock.newCondition();
            new Thread(() -> {
          
          
                try {
          
          
                    reentrantLock.lock();
                    log.info("wait signal"); // 1
                    condition.await();
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
                log.info("get signal"); // 4
                reentrantLock.unlock();
            }).start();
            new Thread(() -> {
          
          
                reentrantLock.lock();
                log.info("get lock"); // 2
                try {
          
          
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
                condition.signalAll();
                log.info("send signal ~ "); // 3
                reentrantLock.unlock();
            }).start();
        }
    }
    

Hilo local

  • Descripción general

ThreadLocal es proporcionado por JDK y admite variables locales de subprocesos. Esto significa que la variable almacenada en ThreadLocal pertenece al subproceso actual y la variable está aislada de otros subprocesos, lo que significa que la variable es exclusiva del subproceso actual.

Si creamos una variable ThreadLocal, cada hilo que accede a esta variable tendrá una copia local de esta variable. Cuando varios hilos operan en esta variable, en realidad operan una copia local de la variable, evitando así la necesidad de hilos para Pregunta de seguridad

  • Ejemplo de uso

    Utilice ThreadLocal para guardar e imprimir información variable relacionada

    public class ThreadLocalTest {
          
          
        private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    
        public static void main(String[] args) {
          
          
    		//创建第一个线程
            Thread threadA = new Thread(() -> {
          
          
                threadLocal.set("ThreadA:" + Thread.currentThread().getName());
                System.out.println("线程A本地变量中的值为:" + threadLocal.get());
            });
    		//创建第二个线程
            Thread threadB = new Thread(() -> {
          
          
                threadLocal.set("ThreadB:" + Thread.currentThread().getName());
                System.out.println("线程B本地变量中的值为:" + threadLocal.get());
            });
    		//启动线程A和线程B
            threadA.start();
            threadB.start();
        }
    }
    

    Ejecute el programa y la información impresa es la siguiente:

    线程A本地变量中的值为:ThreadAThread-0
    线程B本地变量中的值为:ThreadBThread-1
    

    En este momento, agregamos una operación de eliminación de variable para el hilo A:

    public class ThreadLocalTest {
          
          
        private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    
        public static void main(String[] args) {
          
          
    		//创建第一个线程
            Thread threadA = new Thread(() -> {
          
          
                threadLocal.set("ThreadA:" + Thread.currentThread().getName());
                System.out.println("线程A本地变量中的值为:" + threadLocal.get());
                threadLocal.remove();
                System.out.println("线程A删除本地变量后ThreadLocal中的值为:" + threadLocal.get());
            });
    		//创建第二个线程
            Thread threadB = new Thread(() -> {
          
          
                threadLocal.set("ThreadB:" + Thread.currentThread().getName());
                System.out.println("线程B本地变量中的值为:" + threadLocal.get());
                System.out.println("线程B没有删除本地变量:" + threadLocal.get());
            });
    		//启动线程A和线程B
            threadA.start();
            threadB.start();
        }
    }
    

    La información impresa es la siguiente:

    线程A本地变量中的值为:ThreadAThread-0
    线程B本地变量中的值为:ThreadBThread-1
    线程B没有删除本地变量:ThreadBThread-1
    线程A删除本地变量后ThreadLocal中的值为:null
    

    A través del programa anterior, podemos ver que las variables almacenadas en ThreadLocal por el subproceso A y el subproceso B no interfieren entre sí. Las variables almacenadas por el subproceso A solo pueden ser accedidas por el subproceso A, y las variables almacenadas por el subproceso B solo pueden acceder ser accedido por el hilo B.

  • Principio ThreadLocal

    public class Thread implements Runnable {
          
          
        /***********省略N行代码*************/
        ThreadLocal.ThreadLocalMap threadLocals = null;
        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    /***********省略N行代码*************/
    }
    

    Como se puede ver en el código fuente anterior, hay threadLocals y heredablesThreadLocals en la clase ThreadLocal. Ambas variables son variables de tipo ThreadLocalMap y los valores iniciales de ambas son nulos. Solo la primera vez que el hilo actual llama al conjunto () o el método get() crearán una instancia de la variable.

    Cabe señalar que las variables locales de cada hilo no se almacenan en la instancia de ThreadLocal, sino en la variable threadLocals del hilo que llama, es decir, cuando se llama al método set () de ThreadLocal, las variables locales almacenadas se almacenan en En el espacio de memoria del hilo de llamada específico, ThreadLocal solo proporciona los métodos get() y set() para acceder a los valores de las variables locales; cuando se llama al método set(), el valor que se establecerá se almacena en los threadLocals del hilo de llamada. .Al llamar Cuando se utiliza el método get() para obtener el valor, las variables almacenadas en threadLocals se obtendrán del hilo actual;

    1. colocar()

      public void set(T value) {
              
              
          //获取当前线程
          Thread t = Thread.currentThread();
          //以当前线程为key,获取ThreadLocalMap对象
          ThreadLocalMap map = getMap(t);
          if (map != null)
              //获取的获取ThreadLocalMap不为空,则赋值操作
              map.set(this, value);
          else
              //获取ThreadLocalMap为空,则为当前线程创建并赋值
              createMap(t, value);
      }
      
      /**
           * Create the map associated with a ThreadLocal. Overridden in
           * InheritableThreadLocal.
           *
           * @param t the current thread
           * @param firstValue value for the initial entry of the map
           */
          void createMap(Thread t, T firstValue) {
              
              
              t.threadLocals = new ThreadLocalMap(this, firstValue);
          }
      
    2. conseguir()

      /**
       * Returns the value in the current thread's copy of this
       * thread-local variable.  If the variable has no value for the
       * current thread, it is first initialized to the value returned
       * by an invocation of the {@link #initialValue} method.
       *
       * @return the current thread's value of this thread-local
       */
      public T get() {
              
              
          //获取当前线程
          Thread t = Thread.currentThread();
          //获取当前线程ThreadLocalMap
          ThreadLocalMap map = getMap(t);
          if (map != null) {
              
              
              //ThreadLocalMap不为空,则从中取值
              ThreadLocalMap.Entry e = map.getEntry(this);
              if (e != null) {
              
              
                  @SuppressWarnings("unchecked")
                  T result = (T)e.value;
                  return result;
              }
          }
          //ThreadLocalMap为空,则返回默认值
          return setInitialValue();
      }
      
    3. eliminar()

      public void remove() {
              
              
          //获取当前线程中的threadLocals
          ThreadLocalMap m = getMap(Thread.currentThread());
          if (m != null)
              //若threadLocals不为空,则清除
              m.remove(this);
      }
      

      Nota: Si el hilo nunca termina, las variables locales siempre existirán en el ThreadLocal del hilo que llama; por lo tanto, si las variables locales no son necesarias, puede llamar al método remove() de ThreadLocal para eliminarlas y evitar problemas de desbordamiento de memoria. .

  • Las variables ThreadLocal no son transitivas

    Las variables locales almacenadas usando ThreadLocal no son transitivas, es decir, para el mismo ThreadLocal, después de establecer el valor en el hilo principal, el valor no se puede obtener en el hilo secundario;

    public class ThreadLocalTest {
          
          
            private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    
            public static void main(String[] args) {
          
          
    			//在主线程中设置值
                threadLocal.set("ThreadLocalTest");
    			//在子线程中获取值
                Thread thread = new Thread(new Runnable() {
          
          
                    @Override
                    public void run() {
          
          
                        System.out.println("子线程获取值:" + threadLocal.get());
                    }
                });
    			//启动子线程
                thread.start();
    			//在主线程中获取值
                System.out.println("主线程获取值:" + threadLocal.get());
            }
        }
    

    Después de ejecutar el código de campo anterior, los resultados se ven así:

    主线程获取值:ThreadLocalTest
    子线程获取值:null
    

    Como se puede ver en el ejemplo anterior, después de establecer el valor de ThreadLocal en el hilo principal, el valor no se puede obtener en el hilo secundario;

    ¿Hay alguna forma de obtener el valor del hilo principal en el hilo secundario? Podemos lograr esto a través de InheritableThreadLocal;

  • HeredableHiloLocal

    La clase InheritableThreadLocal hereda de ThreadLocal, que puede obtener el valor establecido en el hilo principal en el hilo secundario;

    public class ThreadLocalTest {
          
          
            private static InheritableThreadLocal<String> threadLocal = 
                new InheritableThreadLocal<String>();
    
            public static void main(String[] args) {
          
          
    			//在主线程中设置值
                threadLocal.set("ThreadLocalTest");
    			//在子线程中获取值
                Thread thread = new Thread(new Runnable() {
          
          
                    @Override
                    public void run() {
          
          
                        System.out.println("子线程获取值:" + threadLocal.get());
                    }
                });
    			//启动子线程
                thread.start();
    			//在主线程中获取值
                System.out.println("主线程获取值:" + threadLocal.get());
            }
        }
    

    Al ejecutar el programa anterior, los resultados son los siguientes:

    主线程获取值:ThreadLocalTest
    子线程获取值:ThreadLocalTest
    

    Se puede ver que al usar InheritableThreadLocal, el subproceso secundario puede obtener las variables locales establecidas en el subproceso principal;

  • HeredableThreadPrincipio local

    public class InheritableThreadLocal<T> extends ThreadLocal<T> {
          
          
        
        protected T childValue(T parentValue) {
          
          
            return parentValue;
        }
    
        /**
         * Get the map associated with a ThreadLocal.
         *
         * @param t the current thread
         */
        ThreadLocalMap getMap(Thread t) {
          
          
           return t.inheritableThreadLocals;
        }
    
        /**
         * Create the map associated with a ThreadLocal.
         *
         * @param t the current thread
         * @param firstValue value for the initial entry of the table.
         */
        void createMap(Thread t, T firstValue) {
          
          
            t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
        }
    }
    

    Se puede ver en el código fuente que InheritableThreadLocal hereda de ThreadLocal y anula el método childValue()/getMap()/createMap().

    Es decir, cuando se llama al método set () de ThreadLocal, se crea la variable heredableThreadLocals del hilo actual en lugar de la variable threadLocals; en este momento, si el hilo padre crea un hilo hijo, en el constructor del Thread clase, las variables locales del subproceso principal en la variable heredableThreadLocals se copian a los heredablesThreadLocals del subproceso secundario.

Problemas de concurrencia

Problemas de visibilidad

Problema de visibilidad, es decir, si un hilo modifica una variable compartida, otro hilo no puede ver la modificación inmediatamente, esto se debe a que la CPU agrega caché;

Las CPU de un solo núcleo no tienen problemas de visibilidad, solo las CPU de múltiples núcleos tienen problemas de visibilidad; las CPU de un solo núcleo en realidad se ejecutan en serie debido a la programación de intervalos de tiempo de un solo núcleo; mientras que las CPU de múltiples núcleos pueden lograr paralelismo;

Código de muestra de visibilidad:

public class ConcurrentTest {
    
    

    private int num = 0;

    public static void main(String[] args) throws InterruptedException {
    
    
        ConcurrentTest concurrentTest = new ConcurrentTest();
        concurrentTest.threadsTest();
    }

    public void threadsTest() throws InterruptedException {
    
    
        Thread thread = new Thread("test-1") {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 20; i++) {
    
    
                    try {
    
    
                        addNum();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        };

        Thread thread2 = new Thread("test-2") {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 20; i++) {
    
    
                    try {
    
    
                        addNum();

                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        };

        thread.start();
        thread2.start();

        thread.join();
        thread2.join();
        System.out.println("执行完毕");
    }

    private void addNum() throws InterruptedException {
    
    
        Thread.sleep(1000);
        num++;
        System.out.println(Thread.currentThread().getName() + ":" + num);
    }
}

problema de atomicidad

La atomicidad se refiere a la característica de que una o más operaciones no se interrumpen durante la ejecución de la CPU. Una vez que una operación atómica comienza a ejecutarse, continuará hasta el final de la operación sin interrupción.

Los problemas atómicos se refieren a situaciones en las que una o más operaciones se interrumpen durante la ejecución de la CPU;

Cuando un hilo está realizando una operación, la CPU cambia para realizar otras tareas, lo que provoca que la tarea actual se interrumpa, lo que provocará problemas de atomicidad;

En JAVA, los programas concurrentes se escriben en base a subprocesos múltiples, lo que también implica el problema del cambio de CPU entre subprocesos.Es precisamente debido al cambio de CPU entre subprocesos que pueden ocurrir problemas de atomicidad en la programación concurrente;

problema de pedido

Orden significa: el código se ejecuta en el orden de ejecución

Reordenamiento de instrucciones: para optimizar el rendimiento del programa, el compilador o intérprete a veces modifica el orden de ejecución del programa, pero esta modificación puede causar problemas inesperados;

En el caso de un solo subproceso, el reordenamiento de las instrucciones aún puede garantizar que el resultado final sea consistente con el resultado de la ejecución secuencial del programa, pero en el caso de múltiples subprocesos, puede haber problemas;

Problema de orden: para optimizar el programa, la CPU reordena las instrucciones, en este momento el orden de ejecución puede ser inconsistente con el orden de codificación, lo que puede causar problemas de orden;

Resumir

Hay tres razones principales por las que la programación concurrente puede causar problemas: problemas de visibilidad causados ​​por el caché, problemas de atomicidad causados ​​por el cambio de subprocesos de la CPU y problemas de orden causados ​​por la reordenación de las instrucciones de optimización del rendimiento.

Principio de sucede antes

En la versión JDK1.5, el modelo de memoria JAVA introduce el principio sucede antes

Código de muestra 1:

class VolatileExample {
    
    
    int x = 0;
    volatile boolean v = false;

    public void writer() {
    
    
        x = 1;
        v = true;
    }

    public void reader() {
    
    
        if (v == true) {
    
    
            //x的值是多少呢?
        }
    }
}
  1. reglas de orden del programa

    ​ En un solo hilo, de acuerdo con el orden del código, la operación anterior ocurre antes de cualquier operación posterior.

    ​ Por ejemplo: En el Ejemplo 1, el código x = 1 se completará antes de v = true;

  2. Reglas de variables volátiles

    Para una operación de escritura volátil, sucede antes de las operaciones de lectura posteriores en ella.

  3. Reglas de entrega

    Si A sucede antes de B, B sucede antes de C, entonces hay A sucede antes de C

    Combinando los principios 1, 2, 3 y el código de muestra 1, podemos sacar las siguientes conclusiones:

    • x = 1 sucede antes v = verdadero, consistente con el principio 1;
    • Escribir variable v = verdadero Sucede antes Leer variable v = verdadero, consistente con el principio 2
    • Luego, de acuerdo con el principio 3, x = 1 Sucede antes de leer la variable v = verdadero;
    • Es decir, si el hilo B lee v = verdadero, entonces x = 1 establecido por el hilo A es visible para B, lo que significa que el hilo B en este momento puede acceder a x = 1
  4. Reglas de bloqueo

    La operación de desbloqueo de una cerradura ocurre antes de la posterior operación de bloqueo de la cerradura.

  5. Reglas de inicio de hilos

    Si el subproceso A llama al inicio () del subproceso B para iniciar el subproceso B, entonces el método start () ocurre antes de cualquier operación en el subproceso B.

    //在线程A中初始化线程B
    Thread threadB = new Thread(()->{
          
          
        //此处的变量x的值是多少呢?答案是100
    });
    //线程A在启动线程B之前将共享变量x的值修改为100
    x = 100;
    //启动线程B
    threadB.start();
    
  6. Reglas de terminación de hilos

    El subproceso A espera a que se complete el subproceso B (llame al método join () del subproceso B). Cuando el subproceso B se completa (el método join () del subproceso B regresa), el subproceso A puede acceder a la modificación de la variable compartida por parte del subproceso B.

    Thread threadB = new Thread(()-{
          
          
    //在线程B中,将共享变量x的值修改为100 
            x = 100;
    });
    //在线程A中启动线程B
    threadB.start();
    //在线程A中等待线程B执行完成
    threadB.join();
    //此处访问共享变量x的值为100
    
  7. Reglas de interrupción del hilo

    Se produce una llamada al método de interrupción () del subproceso, antes de que el subproceso interrumpido detecte que se ha producido el evento de interrupción.

  8. Reglas de finalización de objetos

    ​ Sucede antes de que se complete la inicialización de un objeto al comienzo del método finilize() del objeto

Marco ForkJoin

  • Descripción general

    Java 1.7 introduce un nuevo marco de concurrencia: el marco Fork/Join; se utiliza principalmente para implementar el algoritmo "divide y vencerás", especialmente las funciones que se llaman recursivamente después de dividir y conquistar.

    La esencia del marco Fork / Join es un marco para ejecutar tareas en paralelo, que puede dividir una tarea grande en varias tareas pequeñas y, finalmente, agregar los resultados de cada tarea pequeña para obtener los resultados de la tarea grande. El marco Fork/Join coexiste con ThreadPool y no pretende reemplazar a ThreadPool.

  • principio

    Fork/Join utiliza una cola infinita para guardar las tareas que deben ejecutarse, y la cantidad de subprocesos se pasa a través del constructor. Si no se pasa la cantidad de subprocesos, la cantidad de CPU disponibles en la computadora actual será establecido en el número de subprocesos de forma predeterminada.

    ForkjoinPool utiliza principalmente el método de dividir y conquistar para resolver el problema. Las aplicaciones típicas incluyen un algoritmo de clasificación rápida.

  • Implementación del marco

    1. TenedorJoinPool

      Implementé el grupo de subprocesos en el marco ForkJoin.

    2. TenedorJoinWorkerThread

      Implementar subprocesos en el marco ForkJoin

    3. TenedorJoinTask

      Encapsula datos y sus operaciones correspondientes y admite el paralelismo de datos detallado.

      ForkJoinTask incluye principalmente dos métodos, fork() y join(), para realizar la división y fusión de tareas respectivamente;

      El método fork () es similar al método start () en el método Thread, pero no ejecuta la tarea inmediatamente, sino que la coloca en la cola de ejecución.

      El método join() es diferente del método join() de Thread. No simplemente bloquea el hilo, sino que utiliza el hilo de trabajo para realizar otras tareas. Cuando un hilo de trabajo llama a join(), manejará otras tareas hasta que nota el subproceso secundario.Ejecución completa.

    4. Tarea recursiva

      ForkJoinTask que devuelve resultados implementa Callable

    5. Acción recursiva

      ForkJoinTask sin resultado de retorno implementa Runnalbe

    6. Completador contado

      Una vez completada la ejecución de la tarea, se activará la ejecución de una función de enlace personalizada.

  • Código de muestra

    public class ForkJoinTaskExample extends RecursiveTask<Integer> {
          
          
        public static final int threshold = 2;
        private int start;
        private int end;
    
        public ForkJoinTaskExample(int start, int end) {
          
          
            this.start = start;
            this.end = end;
        }
    
        @Override
        protected Integer compute() {
          
          
            int sum = 0;
    		//如果任务足够小就计算任务
            boolean canCompute = (end - start) <= threshold;
            if (canCompute) {
          
          
                for (int i = start; i <= end; i++) {
          
          
                    sum += i;
                }
            } else {
          
          
    			// 如果任务大于阈值,就分裂成两个子任务计算
                int middle = (start + end) / 2;
                ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
                ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle + 1, end);
    			// 执行子任务
                leftTask.fork();
                rightTask.fork();
    			// 等待任务执行结束合并其结果
                int leftResult = leftTask.join();
                int rightResult = rightTask.join();
    			// 合并子任务
                sum = leftResult + rightResult;
            }
            return sum;
        }
    
        public static void main(String[] args) {
          
          
            ForkJoinPool forkjoinPool = new ForkJoinPool();
    		//生成一个计算任务,计算1+2+3+4
            ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);
    		//执行一个任务
            Future<Integer> result = forkjoinPool.submit(task);
            try {
          
          
                log.info("result:{}", result.get());
            } catch (Exception e) {
          
          
                log.error("exception", e);
            }
        }
    }
    

completado antes de v = verdadero;

  1. Reglas de variables volátiles

    Para una operación de escritura volátil, sucede antes de las operaciones de lectura posteriores en ella.

  2. Reglas de entrega

    Si A sucede antes de B, B sucede antes de C, entonces hay A sucede antes de C

    Combinando los principios 1, 2, 3 y el código de muestra 1, podemos sacar las siguientes conclusiones:

    • x = 1 sucede antes v = verdadero, consistente con el principio 1;
    • Escribir variable v = verdadero Sucede antes Leer variable v = verdadero, consistente con el principio 2
    • Luego, de acuerdo con el principio 3, x = 1 Sucede antes de leer la variable v = verdadero;
    • Es decir, si el hilo B lee v = verdadero, entonces x = 1 establecido por el hilo A es visible para B, lo que significa que el hilo B en este momento puede acceder a x = 1
  3. Reglas de bloqueo

    La operación de desbloqueo de una cerradura ocurre antes de la posterior operación de bloqueo de la cerradura.

  4. Reglas de inicio de hilos

    Si el subproceso A llama al inicio () del subproceso B para iniciar el subproceso B, entonces el método start () ocurre antes de cualquier operación en el subproceso B.

    //在线程A中初始化线程B
    Thread threadB = new Thread(()->{
          
          
        //此处的变量x的值是多少呢?答案是100
    });
    //线程A在启动线程B之前将共享变量x的值修改为100
    x = 100;
    //启动线程B
    threadB.start();
    
  5. Reglas de terminación de hilos

    El subproceso A espera a que se complete el subproceso B (llame al método join () del subproceso B). Cuando el subproceso B se completa (el método join () del subproceso B regresa), el subproceso A puede acceder a la modificación de la variable compartida por parte del subproceso B.

    Thread threadB = new Thread(()-{
          
          
    //在线程B中,将共享变量x的值修改为100 
            x = 100;
    });
    //在线程A中启动线程B
    threadB.start();
    //在线程A中等待线程B执行完成
    threadB.join();
    //此处访问共享变量x的值为100
    
  6. Reglas de interrupción del hilo

    Se produce una llamada al método de interrupción () del subproceso, antes de que el subproceso interrumpido detecte que se ha producido el evento de interrupción.

  7. Reglas de finalización de objetos

    ​ Sucede antes de que se complete la inicialización de un objeto al comienzo del método finilize() del objeto

Marco ForkJoin

  • Descripción general

    Java 1.7 introduce un nuevo marco de concurrencia: el marco Fork/Join; se utiliza principalmente para implementar el algoritmo "divide y vencerás", especialmente las funciones que se llaman recursivamente después de dividir y conquistar.

    La esencia del marco Fork / Join es un marco para ejecutar tareas en paralelo, que puede dividir una tarea grande en varias tareas pequeñas y, finalmente, agregar los resultados de cada tarea pequeña para obtener los resultados de la tarea grande. El marco Fork/Join coexiste con ThreadPool y no pretende reemplazar a ThreadPool.

  • principio

    Fork/Join utiliza una cola infinita para guardar las tareas que deben ejecutarse, y la cantidad de subprocesos se pasa a través del constructor. Si no se pasa la cantidad de subprocesos, la cantidad de CPU disponibles en la computadora actual será establecido en el número de subprocesos de forma predeterminada.

    ForkjoinPool utiliza principalmente el método de dividir y conquistar para resolver el problema. Las aplicaciones típicas incluyen un algoritmo de clasificación rápida.

  • Implementación del marco

    1. TenedorJoinPool

      Implementé el grupo de subprocesos en el marco ForkJoin.

    2. TenedorJoinWorkerThread

      Implementar subprocesos en el marco ForkJoin

    3. TenedorJoinTask

      Encapsula datos y sus operaciones correspondientes y admite el paralelismo de datos detallado.

      ForkJoinTask incluye principalmente dos métodos, fork() y join(), para realizar la división y fusión de tareas respectivamente;

      El método fork () es similar al método start () en el método Thread, pero no ejecuta la tarea inmediatamente, sino que la coloca en la cola de ejecución.

      El método join() es diferente del método join() de Thread. No simplemente bloquea el hilo, sino que utiliza el hilo de trabajo para realizar otras tareas. Cuando un hilo de trabajo llama a join(), manejará otras tareas hasta que nota el subproceso secundario.Ejecución completa.

    4. Tarea recursiva

      ForkJoinTask que devuelve resultados implementa Callable

    5. Acción recursiva

      ForkJoinTask sin resultado de retorno implementa Runnalbe

    6. Completador contado

      Una vez completada la ejecución de la tarea, se activará la ejecución de una función de enlace personalizada.

  • Código de muestra

    public class ForkJoinTaskExample extends RecursiveTask<Integer> {
          
          
        public static final int threshold = 2;
        private int start;
        private int end;
    
        public ForkJoinTaskExample(int start, int end) {
          
          
            this.start = start;
            this.end = end;
        }
    
        @Override
        protected Integer compute() {
          
          
            int sum = 0;
    		//如果任务足够小就计算任务
            boolean canCompute = (end - start) <= threshold;
            if (canCompute) {
          
          
                for (int i = start; i <= end; i++) {
          
          
                    sum += i;
                }
            } else {
          
          
    			// 如果任务大于阈值,就分裂成两个子任务计算
                int middle = (start + end) / 2;
                ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
                ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle + 1, end);
    			// 执行子任务
                leftTask.fork();
                rightTask.fork();
    			// 等待任务执行结束合并其结果
                int leftResult = leftTask.join();
                int rightResult = rightTask.join();
    			// 合并子任务
                sum = leftResult + rightResult;
            }
            return sum;
        }
    
        public static void main(String[] args) {
          
          
            ForkJoinPool forkjoinPool = new ForkJoinPool();
    		//生成一个计算任务,计算1+2+3+4
            ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);
    		//执行一个任务
            Future<Integer> result = forkjoinPool.submit(task);
            try {
          
          
                log.info("result:{}", result.get());
            } catch (Exception e) {
          
          
                log.error("exception", e);
            }
        }
    }
    

Supongo que te gusta

Origin blog.csdn.net/weixin_40709965/article/details/128160545
Recomendado
Clasificación