Preguntas de la entrevista sobre subprocesos múltiples de Java
Un proceso es un entorno de ejecución autónomo, que puede considerarse como un programa o una aplicación. Un hilo es una tarea ejecutada en un proceso. El entorno de ejecución de Java es un proceso único que contiene diferentes clases y programas. Los subprocesos pueden denominarse procesos ligeros. Los subprocesos requieren menos recursos para crear y residir en un proceso, y pueden compartir recursos dentro del proceso.
En un programa de subprocesos múltiples, se ejecutan varios subprocesos al mismo tiempo para mejorar la eficiencia del programa. La CPU no entrará en un estado inactivo porque un subproceso necesita esperar recursos. Varios subprocesos comparten memoria de montón, por lo que es mejor crear varios subprocesos para realizar algunas tareas que crear múltiples procesos. Por ejemplo, los servlets son mejores que CGI porque los servlets admiten subprocesos múltiples, mientras que CGI no.
Cuando creamos un hilo en un programa Java, se llama hilo de usuario. Un subproceso de demonio es un subproceso que se ejecuta en segundo plano y no impide que la JVM finalice. Cuando no se están ejecutando subprocesos de usuario, la JVM cierra el programa y sale. Los subprocesos secundarios creados por un subproceso de demonio siguen siendo subprocesos de demonio.
Hay dos formas de crear un hilo: una es implementar la interfaz Runnable y luego pasarla al constructor Thread para crear un objeto Thread y la otra es heredar directamente la clase Thread; Si quieres saber más puedes leer este artículo sobre cómo crear hilos en Java.
Cuando creamos un nuevo hilo en un programa Java, su estado es Nuevo. Cuando llamamos al método start() del hilo, el estado cambia a Runnable. El programador de subprocesos asigna tiempo de CPU a los subprocesos en el grupo de subprocesos ejecutables y cambia su estado a En ejecución. Otros estados del hilo incluyen Esperando, Bloqueado y Muerto. Lea este artículo para obtener más información sobre el ciclo de vida de los subprocesos.
Por supuesto, pero si llamamos al método run() de Thread, se comportará como un método normal. Para ejecutar nuestro código en un nuevo hilo, debemos usar el método Thread.start().
Podemos usar el método Sleep() de la clase Thread para pausar el hilo por un período de tiempo. Cabe señalar que esto no finaliza el subproceso. Una vez que el subproceso se despierta del modo de suspensión, el estado del subproceso cambiará a Ejecutable y se ejecutará de acuerdo con el cronograma del subproceso.
Cada subproceso tiene una prioridad. En términos generales, los subprocesos de alta prioridad tendrán prioridad cuando se ejecuten, pero esto depende de la implementación de la programación de subprocesos, que depende del sistema operativo. Podemos definir la prioridad de los subprocesos, pero esto no garantiza que los subprocesos de alta prioridad se ejecuten antes que los de baja prioridad. La prioridad del subproceso es una variable int (de 1 a 10), 1 representa la prioridad más baja y 10 representa la prioridad más alta.
El programador de subprocesos es un servicio del sistema operativo que se encarga de asignar tiempo de CPU a los subprocesos en estado Ejecutable. Una vez que creamos un hilo y lo iniciamos, su ejecución depende de la implementación del programador de hilos. La división de tiempo se refiere al proceso de asignar tiempo de CPU disponible a los subprocesos ejecutables disponibles. La asignación de tiempo de CPU puede basarse en la prioridad del subproceso o en el tiempo de espera del subproceso. La programación de subprocesos no está controlada por la máquina virtual Java, por lo que es mejor que la aplicación la controle (es decir, no haga que su programa dependa de la prioridad de los subprocesos).
El cambio de contexto es el proceso de almacenar y restaurar el estado de la CPU, lo que permite que la ejecución del subproceso reanude la ejecución desde el punto de interrupción. El cambio de contexto es una característica esencial de los sistemas operativos multitarea y los entornos multiproceso.
Podemos usar el método joint() de la clase Thread para asegurarnos de que todos los subprocesos creados por el programa finalicen antes de que salga el método main(). Aquí hay un artículo sobre el método joint() de la clase Thread.
Cuando los recursos se pueden compartir entre subprocesos, la comunicación entre subprocesos es un medio importante para coordinarlos. Los métodos wait()/notify()/notifyAll() en la clase Object se pueden utilizar para comunicarse entre subprocesos sobre el estado de los bloqueos de recursos. Haga clic aquí para obtener más información sobre esperar hilos, notificar y notificar a todos.
Cada objeto en Java tiene un bloqueo (monitor, que también puede ser un monitor), y métodos como esperar () y notificar () se utilizan para esperar el bloqueo del objeto o notificar a otros subprocesos que el monitor del objeto está disponible. No hay bloqueos ni sincronizadores disponibles para ningún objeto en los subprocesos de Java. Es por eso que estos métodos son parte de la clase Object para que cada clase en Java tenga métodos básicos para la comunicación entre subprocesos.
Cuando un hilo necesita llamar al método esperar () de un objeto, el hilo debe poseer el bloqueo del objeto. Luego liberará el bloqueo del objeto y entrará en el estado de espera hasta que otros hilos llamen al método notificar () en el objeto. De manera similar, cuando un subproceso necesita llamar al método notify() del objeto, liberará el bloqueo del objeto para que otros subprocesos en espera puedan obtener el bloqueo del objeto. Dado que todos estos métodos requieren que el hilo mantenga el bloqueo del objeto, lo que solo se puede lograr mediante la sincronización, solo se pueden llamar en métodos sincronizados o bloques sincronizados.
Los métodos sleep() y yield() de la clase Thread se ejecutarán en el hilo que se está ejecutando actualmente. Por lo tanto, no tiene sentido llamar a estos métodos en otros subprocesos que están en espera. Por eso estos métodos son estáticos. Pueden funcionar en el subproceso que se está ejecutando actualmente y evitar que los programadores piensen erróneamente que estos métodos se pueden llamar en otros subprocesos que no se están ejecutando.
Hay muchas formas de garantizar la seguridad de los subprocesos en Java: sincronización, uso de clases atómicas concurrentes, implementación de bloqueos concurrentes, uso de la palabra clave volátil, uso de clases inmutables y clases seguras para subprocesos. Puede obtener más información en el tutorial de seguridad de subprocesos.
Cuando usamos la palabra clave volátil para modificar una variable, el hilo leerá la variable directamente y no la almacenará en caché. Esto asegura que las variables leídas por el hilo sean consistentes con las de la memoria.
Un bloque sincronizado es una mejor opción porque no bloquea todo el objeto (por supuesto, también puede hacer que bloquee todo el objeto). Los métodos sincronizados bloquean todo el objeto, incluso si hay varios bloques sincronizados no relacionados en la clase, lo que generalmente hace que dejen de ejecutarse y deban esperar para obtener el bloqueo del objeto.
El hilo se puede configurar como un hilo de demonio usando el método setDaemon(true) de la clase Thread. Cabe señalar que este método debe llamarse antes de llamar al método start(); de lo contrario, se generará una excepción IllegalThreadStateException.
ThreadLocal se usa para crear variables locales de subprocesos. Sabemos que todos los subprocesos de un objeto compartirán sus variables globales, por lo que estas variables no son seguras para subprocesos. Pero cuando no queremos utilizar la sincronización, podemos elegir variables ThreadLocal.
Cada hilo tendrá sus propias variables de hilo y pueden usar los métodos get()/set() para obtener sus valores predeterminados o cambiar sus valores dentro del hilo. Las instancias ThreadLocal normalmente quieren que el estado de su hilo asociado sea propiedades estáticas privadas. En el artículo de ejemplo de ThreadLocal puedes ver un pequeño programa sobre ThreadLocal.
ThreadGroup es una clase cuyo propósito es proporcionar información sobre grupos de subprocesos.
La API ThreadGroup es relativamente débil y no proporciona más funciones que Thread. Tiene dos funciones principales: una es obtener la lista de subprocesos activos en el grupo de subprocesos; la otra es configurar el controlador de excepciones no detectadas para el subproceso. Sin embargo, en Java 1.5, la clase Thread también agregó el método setUncaughtExceptionHandler (UncaughtExceptionHandler eh), por lo que ThreadGroup está obsoleto y no se recomienda continuar usándolo.
t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){ @Overridepublic void uncaughtException(Thread t, Throwable e) {System.out.println("se produjo una excepción:"+e.getMessage());} });
Un volcado de subprocesos es una lista de subprocesos activos de JVM, lo cual es muy útil para analizar cuellos de botella y puntos muertos del sistema. Hay muchas formas de obtener volcados de subprocesos: utilizando Profiler, el comando Kill -3, la herramienta jstack, etc. Prefiero la herramienta jstack porque es fácil de usar y viene con JDK. Dado que es una herramienta basada en terminal, podemos escribir algunos scripts para generar periódicamente volcados de subprocesos para su análisis. Lea este documento para obtener más información sobre cómo generar volcados de subprocesos.
El punto muerto se refiere a una situación en la que más de dos subprocesos están bloqueados para siempre. Esta situación requiere al menos dos subprocesos más y más de dos recursos.
Para analizar el punto muerto, debemos observar el volcado de subprocesos de la aplicación Java. Necesitamos averiguar qué subprocesos están en estado BLOQUEADO y los recursos que están esperando. Cada recurso tiene una identificación única; usando esta identificación podemos averiguar qué subprocesos ya poseen su bloqueo de objeto.
Evitar bloqueos anidados, usar bloqueos solo cuando sea necesario y evitar esperas indefinidas son formas comunes de evitar interbloqueos. Lea este artículo para aprender a analizar interbloqueos.
java.util.Timer es una clase de herramienta que se puede utilizar para programar un hilo para que se ejecute en un momento específico en el futuro. La clase Timer se puede utilizar para programar tareas únicas o tareas periódicas.
java.util.TimerTask es una clase abstracta que implementa la interfaz Runnable. Necesitamos heredar esta clase para crear nuestras propias tareas programadas y usar Timer para programar su ejecución.
A continuación se muestran ejemplos sobre el temporizador de Java.
Un grupo de subprocesos gestiona un grupo de subprocesos de trabajo y también incluye una cola para colocar tareas en espera de ser ejecutadas.
java.util.concurrent.Executors proporciona una implementación de la interfaz java.util.concurrent.Executor para crear grupos de subprocesos. El ejemplo de Thread Pool muestra cómo crear y utilizar un grupo de subprocesos, o lea el ejemplo de ScheduledThreadPoolExecutor para aprender cómo crear una tarea periódica.
Preguntas de la entrevista de concurrencia de Java
Una operación atómica se refiere a una unidad de tarea operativa que no se ve afectada por otras operaciones. Las operaciones atómicas son un medio necesario para evitar la inconsistencia de los datos en un entorno de subprocesos múltiples.
int++ no es una operación atómica, por lo que cuando un hilo lee su valor y agrega 1, otro hilo puede leer el valor anterior, lo que provocará un error.
Para resolver este problema, debemos asegurarnos de que la operación de aumento sea atómica. Antes de JDK1.5, podíamos usar tecnología de sincronización para hacer esto. A partir de JDK 1.5, el paquete java.util.concurrent.atomic proporciona clases de carga de tipos int y long que garantizan automáticamente que sus operaciones sean atómicas y no requieran el uso de sincronización. Puede leer este artículo para aprender sobre las clases atómicas de Java.
La interfaz Lock proporciona operaciones de bloqueo más escalables que los métodos sincronizados y los bloques sincronizados. Permiten estructuras más flexibles que pueden tener propiedades completamente diferentes y pueden admitir múltiples clases relacionadas de objetos condicionales.
Sus ventajas son:
Leer más sobre ejemplos de bloqueo
El marco Executor se introdujo en Java 5 con la interfaz java.util.concurrent.Executor. El marco Executor es un marco para tareas asincrónicas que se llaman, programan, ejecutan y controlan de acuerdo con un conjunto de estrategias de ejecución.
La creación ilimitada de subprocesos puede provocar que la memoria de la aplicación se desborde. Por lo tanto, crear un grupo de subprocesos es una mejor solución porque la cantidad de subprocesos puede ser limitada y estos subprocesos se pueden reciclar y reutilizar. Es muy conveniente crear un grupo de subprocesos utilizando el marco Executors. Lea este artículo para aprender cómo crear un grupo de subprocesos utilizando el marco Executor.
Las características de java.util.concurrent.BlockingQueue son: cuando la cola está vacía, se bloqueará la operación de obtener o eliminar elementos de la cola, o cuando la cola esté llena, se bloqueará la operación de agregar elementos a la cola .
La cola de bloqueo no acepta valores nulos. Cuando intenta agregar un valor nulo a la cola, generará una NullPointerException.
Las implementaciones de colas de bloqueo son seguras para subprocesos y todos los métodos de consulta son atómicos y utilizan bloqueos internos u otras formas de control de concurrencia.
La interfaz BlockingQueue es parte del marco de colecciones de Java y se utiliza principalmente para implementar el problema productor-consumidor.
Lea este artículo para aprender cómo implementar el problema productor-consumidor mediante colas de bloqueo.
Java 5 introdujo la interfaz java.util.concurrent.Callable en el paquete de concurrencia, que es muy similar a la interfaz Runnable, pero puede devolver un objeto o generar una excepción.
La interfaz Callable utiliza genéricos para definir su tipo de devolución. La clase Executors proporciona algunos métodos útiles para ejecutar tareas dentro de Callable en el grupo de subprocesos. Dado que la tarea invocable es paralela, tenemos que esperar el resultado que devuelve. El objeto java.util.concurrent.Future nos resuelve este problema. Después de que el grupo de subprocesos envía la tarea invocable, se devuelve un objeto futuro. Utilizándolo, podemos conocer el estado de la tarea invocable y obtener el resultado de la ejecución devuelto por invocable. Future proporciona el método get() para que podamos esperar a que finalice el invocable y obtener los resultados de su ejecución.
Lea este artículo para conocer más ejemplos sobre Callable, Future.
FutureTask es una implementación básica de Future, que podemos usar con Executors para procesar tareas asincrónicas. Por lo general, no necesitamos usar la clase FutureTask, pero resulta muy útil cuando planeamos anular algunos métodos de la interfaz Future y mantener la implementación básica original. Podemos simplemente heredar de él y anular los métodos que necesitemos. Lea el ejemplo de Java FutureTask para aprender a usarlo.
Las clases de colección de Java son rápidas, lo que significa que cuando se modifica la colección y un subproceso usa un iterador para recorrer la colección, el método next() del iterador generará una excepción ConcurrentModificationException.
Los contenedores simultáneos admiten recorridos simultáneos y actualizaciones simultáneas.
Las clases principales son ConcurrentHashMap, CopyOnWriteArrayList y CopyOnWriteArraySet. Lea este artículo para aprender cómo evitar ConcurrentModificationException.
Los ejecutores proporcionan algunos métodos de utilidad para las clases Executor, ExecutorService, ScheduledExecutorService, ThreadFactory y Callable.
Los ejecutores se pueden utilizar para crear fácilmente grupos de subprocesos.
Texto original: journaldev.com Traducción: ifeve Traductor: Zheng Xudong