[Notas de estudio de Java - Programación concurrente] Subprocesos y tareas

prefacio

Recientemente, estuve mirando algunas soluciones de implementación, programación de subprocesos y concurrencia de Java 15. Aunque muchas cosas siguen siendo 1.5, siguen siendo muy gratificantes.

1. Hilos y tareas

En Java, los subprocesos se utilizan para ejecutar tareas y se puede decir que los subprocesos son contenedores de tareas. Sin el inicio físico del subproceso (start0), no se ejecutará ninguna tarea.

Si ha leído el código fuente de Thread, puede saber que la implementación de hilos en Java es muy cerrada, y su mecanismo proviene del método de hilo p de bajo nivel de c. En el código fuente, a través de la palabra clave nativa, apoyándose en la interfaz JNI, llame a otros lenguajes para lograr el acceso a la capa inferior.

2. La interfaz básica de subprocesos y tareas de Java

En los paquetes concurrentes y lang, hay varias interfaces básicas que deben entenderse primero:

  • Ejecutable
  • Llamable
  • Futuro

Ejecutable

Para tareas o subprocesos , la función más básica y principal es ejecutar , por lo que en Java1.0, solo existe la interfaz Runnable. La especificación de la interfaz Runnable también es muy simple:

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

No hay valor de retorno ni excepción, simplemente se ejecuta y el código de ejecución de la tarea se implementa en la función de ejecución. Si necesita obtener el resultado de la ejecución de la tarea, debe escribir una función de devolución de llamada .

Implemente la interfaz Runnable y abstraiga tareas paralelas personalizadas y simplificadas que requieren nuevos subprocesos para ejecutarse de acuerdo con las necesidades comerciales reales.

Thread es una clase de implementación que implementa Runnable, por lo que personalmente entiendo que desde la perspectiva de Java, los hilos son en realidad una tarea especial.

  • Runnable es adecuado como una interfaz implementada para ser implementada por clases de tareas (porque Thread y Executor solo pueden ingresar a Runnable).

Llamable

Pero si es solo esto, no cumple con nuestros requisitos para la gestión de tareas . Queremos obtener la excepción de la tarea lanzada por la tarea de ejecución del subproceso (tenga en cuenta que no es una excepción del subproceso, los dos son esencialmente diferentes) y el resultado devuelto de manera más conveniente. Por lo tanto, hay una interfaz invocable en Java1.5.

La especificación de la interfaz Callable tampoco es complicada:

public interface Callable<V> {
    
    
    V call() throws Exception;
}

Comparado con Runnable, podemos ver cambios obvios. En primer lugar, en el proceso de ejecución de la tarea por parte del subproceso, podemos detectar la excepción lanzada por la tarea . En segundo lugar, podemos obtener el valor de retorno del tipo de entrada.

  • Callable es más adecuado para escribirse en una tarea como contenido de la tarea, ya que puede manejar fácilmente el lanzamiento de excepciones en ejecución. (Esto se reflejará en FutureTask)

Futuro

La interfaz Future también se proporciona en Java 1.5 para administrar tareas con más detalle.

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, TimeoutException;
}

En esta especificación de interfaz, podemos monitorear el estado del hilo. En primer lugar, se proporciona una especificación para la terminación manual de subprocesos. En segundo lugar, lo que es más útil es que con la función get, significa que podemos bloquear y obtener los resultados del cálculo del hilo en cualquier momento y lugar (cualquier fila).

3. Clases de implementación básica de hilos y tareas de Java

Después de comprender estas interfaces básicas, veamos varias clases de implementación de subprocesos de Java (Thread) y clases de implementación de tareas clásicas (FutureTask) para ver cómo utiliza Java estas especificaciones.

Hilo

Thread es la clase más simple, directa y de nivel más bajo en Java para crear un objeto de hilo y abrir un hilo.

Tenga en cuenta que la función de ejecución debe distinguirse de la función de inicio. La función de inicio es para iniciar físicamente un hilo, y la función de ejecución es solo para llamar a nuestra implementación de Runnable (el código ejecutado en el hilo, es decir, la tarea).

	//创建对象
    Thread thread = new Thread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            //代码实现
            //如果想让线程回传结果,只能在这里面写回调函数。
        }
    });
    //开启线程
    thread.start();

En el código anterior, podemos entender aproximadamente que una tarea (implementación de Runnable) se coloca en un objeto de subproceso y luego se llama a la función de inicio para permitir que el subproceso calcule la tarea.

Mirando el código fuente, sabemos que el constructor de Thread solo puede pasar la implementación de Runnable. Por lo tanto, si no usa el grupo de subprocesos, debe implementar Runnable cuando escriba su propia clase de tarea comercial (Tarea).

Echemos un vistazo, mencionado anteriormente, donde se inicia realmente el hilo:

    public synchronized void start() {
    
    

        if (threadStatus != 0)
            throw new IllegalThreadStateException();
            
        group.add(this);

        boolean started = false;
        try {
    
    
        	//这里是真正物理层开启线程的地方 start0() 函数~~~
            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 */
            }
        }
    }

    private native void start0();
    
    //仅仅是调用实现
    @Override
    public void run() {
    
    
        if (target != null) {
    
    
            target.run();
        }
    }

Vemos que es la función start0() en start() la que realmente inicia el hilo en la capa física, que es la interfaz JNI mencionada anteriormente, y llama a otros lenguajes para acceder a la capa inferior. En otras palabras, el hilo es creado y ejecutado por start().

La implementación de la función de ejecución Runnable es llamada por la función de ejecución en el subproceso, pero aún no me queda claro cómo se ejecuta la ejecución del subproceso en el subproceso iniciado por start0. Se requiere más estudio.

Además de start0, hay muchas otras operaciones implementadas sin Java, como:

    private native void setPriority0(int newPriority);
    private native void stop0(Object o);
    private native void suspend0();
    private native void resume0();
    private native void interrupt0();
    private static native void clearInterruptEvent();
    private native void setNativeName(String name);

FuturoTarea

FutureTask es la implementación básica de Runnable y Future. De hecho, es la gestión básica de una tarea asincrónica. Podemos leer aproximadamente los detalles de la implementación y proporcionarnos ideas para realizar nuestra propia tarea.

Echemos un vistazo a la relación de herencia de implementación de esta clase:

inserte la descripción de la imagen aquí
Está claro que FutureTask es en realidad una combinación de Runnable y Future. (El concepto de tarea mencionado anteriormente también se refleja aquí).

    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;

    /** 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;

En esta clase, puede ver el estado de la tarea (el estado de la tarea), incluido Callable (contenido de la tarea), el resultado de salida (cualquier objeto) y Thread (el Thread que ejecuta la tarea) y WaitNode (este se mencionará más adelante).

Por supuesto, en los negocios reales, es posible que una tarea simple solo necesite tener una identificación distinta, un identificador para juzgar la ejecución y un bloqueo para satisfacer las funciones básicas.

En el constructor de FutureTask, puede ver claramente el proceso de inicialización del objeto y la esencia de la construcción de esta clase.

    public FutureTask(Callable<V> callable) {
    
    
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;
    }

    public FutureTask(Runnable runnable, V result) {
    
    
    	//Executors 运用了 RunnableAdapter 将 Runnable 转为 Callable<T>
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;   
    }

Se puede ver que incluso con el segundo constructor, Runnable se convierte en Callable internamente.

Eche otro vistazo a la función de ejecución:

    public void run() {
    
    
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
    
    
            Callable<V> c = callable;
            if (c != null && state == NEW) {
    
    
                V result;
                boolean ran;
                //这里就是为什么推荐使用Callable作为输入,因为方便catch异常,
                //不然只能在 Runnable 的run中回调。
                try {
    
    
                	//执行自己实现的call函数
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
    
    
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
    
    
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

Podemos ver lo más básico, en la condición de interrupción, la tarea no se puede ejecutar repetidamente y este objeto no puede ejecutar otros hilos. Después de eso, también es el proceso de implementación normal.

El uso de FutureTask también es muy sencillo, aquí se recomienda ingresar a Callable:

    FutureTask<String> f = new FutureTask<>(new Callable<String>() {
    
    
        @Override
        public String call() throws Exception {
    
    
            return null;
        }
    });
    Thread t = new Thread(f);
    t.start();

También es muy obvio aquí que una tarea se coloca en un subproceso para ejecutarse y se obtienen varios estados de la tarea.

Cuatro Resumen

En el aprendizaje de subprocesos múltiples de Java, es necesario comprender la diferencia entre subprocesos y tareas, la diferencia esencial entre Runnable y Callable (no la apariencia del código) y la conexión entre ellos.

Callable es más adecuado para escribirse en la tarea como un contenido de la tarea (porque las excepciones se pueden manejar fácilmente en ejecución), y Runnable es adecuado para ser implementado por la clase de tarea como una interfaz implementada (porque Thread y Executor solo pueden ingresar Runnable) . Esto se refleja completamente en la clase FutureTask, experimentémoslo nuevamente:

  • Por qué se recomienda implementar Runnable
	//新建任务
    FutureTask<String> f = new FutureTask<>(new Callable<String>() {
    
    
        @Override
        public String call() throws Exception {
    
    
            return null;
        }
    });
    //为什么推荐 implements Runnable
    Thread t = new Thread(f);
    //物理开启线程
    t.start();
  • ¿Por qué se recomienda la entrada invocable al constructor?
    public void run() {
    
    
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
    
    
            Callable<V> c = callable;
            if (c != null && state == NEW) {
    
    
                V result;
                boolean ran;
                //这里就是为什么推荐使用Callable作为输入,因为方便catch异常,
                //不然只能在 Runnable 的run中回调。
                try {
    
    
                	//执行自己实现的call函数
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
    
    
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
    
    
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

Recuerde siempre que un subproceso es un contenedor para una tarea, y el inicio físico del subproceso es independiente del código de implementación de la tarea. De esta manera, podemos tener una comprensión más profunda de la naturaleza de los subprocesos múltiples de Java, y nos será útil comprender el grupo de subprocesos más adelante.

Supongo que te gusta

Origin blog.csdn.net/weixin_43742184/article/details/113388988
Recomendado
Clasificación