Mostrando las entradas con la etiqueta Máquina Virtual de Java. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Máquina Virtual de Java. Mostrar todas las entradas

3 de junio de 2021

Ejercicios selectos (excepciones).

  1. Con base en lo descrito en la entrada Excepciones, considere el Ejemplo DivisionConManejoExepcion2 y compárese en el ejemplo de dicha entrada (Ejemplo DivisionConManejoExepcion). La equivalencia lógica y funcional es la misma, note que la diferencia radica en la cláusula finally del ejemplo en cuestión (líneas 41-44). Asegúrese de comprender las diferencias así como su equivalencia.
  2. Escriba una clase de prueba que permita probar el funcionamiento de la clase ListOfNumbers.
  3. En las versiones de Java 7 y posteriores, un bloque catch puede manejar más de un tipo de excepción. Dicha característica pretende reducir la duplicidad de código y tener demasiada extensión en las cláusulas de tipo catch. En este sentido, considere el Ejemplo ListOfNumbers2 que es la versión actualizada del Ejemplo ListOfNumbers visto en la entrada Atrapando y manejando excepciones. Asegúrese de entender su equivalencia antes de continuar.
  4. Tomando en consideración lo descrito en la entrada Especificación de excepciones lanzadas por un método, modifique el Ejemplo ListOfNumbers2 para que el método writeList( ) no maneje la excepción, sino que indique que la puede lanzar. Así mismo, deberá modificar de manera apropiada lo realizado en el Ejercicio 2 (ahora la clase de prueba tendrá que hacer el manejo correspondiente de la excepción) para que el programa siga funcionando.
  5. En la entrada Excepciones se trabajó con el Ejemplo DivisionConManejoExepcion, y en el Ejercicio 1 con una versión alternativa (Ejemplo DivisionConManejoExepcion2). Considere ahora el Ejemplo DivisionConManejoExcepcion3 y asegúrese de comprender las diferencias así como las equivalencias con los ejemplos citados. Note que la única diferencia de este último con respecto de la versión anterior es la cláusula throws ArithmeticException (línea 17). En la entrada Especificación de excepciones lanzadas por un método puede consultar los detalles de dicha cláusula.
  6. Considere una ecuación cuadrática de la forma ax^2 + bx + c. Escriba un programa que permita leer los coeficientes reales de una ecuación de este tipo y obtenga las raíces reales de la misma. Para ello:
    • Si el coeficiente a es cero, deberá generar tanto la excepción como el manejo correspondiente de la misma; es decir, su programa deberá reportar el problema (no es una ecuación cuadrática) y continuar.
    • Detectar si los coeficientes no son números.
    • Si las raíces de la ecuación no son números reales, deberá también reportar dicha situación como una excepción y realizar el manejo correspondiente (preguntar al usuario si desea continuar o no).
  7. Considere el Ejemplo Excepciones el cual contiene comentarios respecto a secciones de código que serían inalcanzables. Compruebe que así sea, para ello, puede agregar una sentencia simple en donde sea pertinente como por ejemplo System.out.println("¿Qué pasará?"); y analice ¿qué sucede?, ¿compila?, ¿se ejecuta?
  8. Para el Ejemplo Excepciones3, ¿que piensa que ocurrirá si se elimina alguna de las cláusulas throws Exception (líneas 10, 16, 22 y 28)? Determine primero su respuesta, y posteriormente compruébese con la experimentación.
  9. Con base en lo expuesto en el ejercicio anterior, ¿qué piensa que sucederá si en lugar de una excepción verificada (checked exception) se utiliza una excepción no verificada (uncheked/runtime exception)? Determine y realice el experimento correspondiente.
  10. Con base en lo expuesto en Lanzado y encadenamiento y en los dos ejercicios anteriores, construya un programa que, como en el Ejemplo Excepciones3 se relance una excepción, con la diferencia de que ahora, en cada método, se deberá atrapar la excepción anterior y encadenarla con una nueva creada en ese método, para relanzar esta nueva excepción y que a su vez sea atrapada y encadenada con una nueva en el método siguiente. Revise nuevamente el Ejemplo Excepciones2 para ver cómo crear un excepción encadenada (compuesta).
  11. A partir de la versión 7 de Java, se incorporó la sentencia try-con-recursos (try-with-resources), la cual permite declarar uno o más recursos dentro de la cláusula. Dichos recursos son objetos que deben ser cerrados cuando el programa que los utiliza ha terminado con ellos. El Ejemplo ListOfNumbers3 es una muestra de cómo funciona dicha cláusula, pero se invita al lector a documentarse más al respecto.

 

2 de junio de 2021

Interbloqueo.

     Al hablar de concurrencia, existe un concepto que se utiliza con frecuencia en el argot de Java: liveness (vivacidad), mismo que podría definirse como la capacidad de una aplicación concurrente de ejecutarse oportunamente. Asociado a este concepto vienen otros estrechamente relacionados: deadlock (interbloqueo) y starvation (inanición).

    El interbloqueo describe una situación en la que dos o más hilos están bloqueados, esperando que uno habilite al otro para su continuación sin que esto nunca ocurra. Considere el siguiente ejemplo:

Alfonso y Gastón son amigos y apasionados fervientes de las estrictas costumbres de cortesía. Una de estas reglas inquebrantables es aquella que dicta que cuando una persona se inclina en afectuoso saludo de reverencia a un amigo, ésta debe permanecer inclinada hasta que el amigo tenga la oportunidad de regresar el saludo con otra reverencia.

    Desafortunadamente para este par de amigos, esta regla no toma en cuenta la posibilidad de que ambas personas pudieran iniciar la reverencia al otro simultáneamente.

    El Ejemplo Interbloqueo modela la situación planteada líneas antes. Cuando este ejemplo se ejecuta, lo más seguro es que ambos hilos se bloqueen cuando intenten invocar al método "regresaReverencia". Asegúrese el lector de comprobar en este momento dicha situación.

    El bloqueo mutuo o interbloqueo nunca terminará debido a que cada hilo está esperando que el otro salga del método "reverencia". Ambos hilos se quedarán esperando por siempre una situación que sólo el otro debe causar pero que nunca ocurrirá.

    En la entrada de Ejercicios se proponen dos ejercicios (10 y 11) relacionados con este ejemplo. Se recomienda ampliamente revisar todos los temas relacionados con hilos y la comprensión del ejemplo aquí presentado antes de intentarlos.

    Finalmente, se comentan dos aspectos estrechamente relacionados con lo anterior:

  1. Según la documentación oficial de Java, la inanición (starvation) describe una situación en la que un hilo no puede obtener un acceso regular a los recursos compartidos y en consecuencia no puede progresar.
  2. Por otro lado, existe una situación similar pero diferente al interbloqueo denominada livelock. La diferencia es sutil pero importante: en un livelock los hilos no están bloqueados sino en ejecución pero estorbándose sin poder continuar. La situación es comparable con la de dos personas tratando de pasar por en pasillo estrecho: Alfonso se mueve a la derecha para permitir que Gastón pase, al tiempo que Gastón se mueve a su izquierda para permitir a Alfonso que pase; si esta situación se repite en el sentido inverso, ambos se seguirán bloqueando sin poder pasar y en consecuencia, sin poder avanzar.


27 de mayo de 2021

Sincronización.

    Los hilos se comunican principalmente compartiendo el acceso a los campos y los objetos a los que hacen referencia dichos campos. Aunque este esquema es extremadamente eficiente, lleva implícito dos errores potenciales:

  1. Interferencia de hilos (Thread interference).
  2. Errores de consistencia de memoria (Memory consistency errors).

    Con la sincronización se pueden prevenir estos dos errores, pero también se puede incurrir en la contención de hilos, que ocurre cuando dos o más hilos tratan de acceder al mismo recurso de manera simultánea (condición de competencia) provocando que el entorno de la máquina virtual ejecute uno o más hilos más lentamente, o incluso que llegue a suspender su ejecución. 

    La inanición y el interbloqueo de hilos son dos formas de contención y su análisis y tratamiento quedan fuera de los alcances de este blog. Aquí sólo se proporcionarán los elementos básicos relacionados con la sincronización de hilos y la prevención de los dos errores mencionados con anterioridad.

Interferencia de hilos.

    El planteamiento iniciará con el análisis de una clase extremadamente simple (Ejemplo Contador) que implementa un contador, así como su respectivo incremento, decremento y acceso a través de los métodos correspondientes.

    Si sólo un hilo accede a los métodos para la modificación de c: todo trabaja como se espera; pero si un objeto de la clase Contador es referido por múltiples hilos, la interferencia entre ellos hará que las cosas se salgan de control.

    La interferencia acontece cuando dos operaciones, ejecutándose en diferentes hilos pero accediendo a los mismos datos, se intercalan. Las operaciones de alto nivel que se escriben en los lenguajes de programación, por simples y sencillas que parezcan, habitualmente consisten de múltiples pasos o suboperaciones, y cuando estos pasos de traslapan o superponen los problemas emergen (condiciones de competencia).

    Los detalles precisos respecto a la exactitud de cómo se descompone una operación tan simple como el incremento de una variable (c++) no son relevantes, basta con saber por ahora que dicha operación podría ser descompuesta en tres pasos (ocurre lo análogo y correspondiente para c--):

  1. Obtener o recuperar el valor actual de c (fetch).
  2. Incrementar dicho valor en 1.
  3. Almacenar el valor incrementado nuevamente en c.

    Con base en lo anterior, supongamos ahora la existencia de dos hilos, y que el Hilo A invoca a incrementa y que casi al mismo tiempo el Hilo B invoca a decrementa. Si el valor inicial de c es 0, una posible secuencia de acciones traslapadas podría ser la siguiente:

  1. Hilo A: Recupera c.
  2. Hilo B: Recupera c.
  3. Hilo A: Incrementa valor recuperado; resultado: 1.
  4. Hilo B: Decrementa valor recuperado; resultado: -1.
  5. Hilo A: Almacena resultado en c; c vale ahora 1.
  6. Hilo B: Almacena resultado en c; c vale ahora -1.

    Como puede observarse, el resultado del Hilo A se pierde: es sobre escrito por el resultado del Hilo B. Este planteamiento es sólo una combinación posible, bajo distintas circunstancias, podría ser que ahora el resultado del Hilo B sea el que se pierda o también podría darse el caso de que todo resulte bien como se espera; el resultado final es impredecible (condición de competencia).

    El Ejemplo PruebaContador muestra la situación de interferencia de hilos en una ejecución con dos hilos: uno (main) incrementando 100 000 veces el contador y el otro (h) decrementando la misma cantidad de veces ¿Cuál es el resultado esperado y cuál es el que se obtiene? ¿Se generan los mismos valores o resultados en distintas ejecuciones? ¿Por qué no son cero?

Errores de consistencia de memoria.

    Este tipo de errores ocurren cuando hilos diferentes tienen vistas inconsistentes de lo que deberían ser los mismos datos. Las causas de este tipo de errores son muchas y muy variadas y su análisis queda fuera de los alcances de este blog. Por ahora basta con saber que el programador debe tener consciencia de este tipo de errores para que pueda emplear una estrategia para anularlos.

    La clave para anular este tipo de errores de consistencia de memoria es entender una relación denominada "sucede antes" (happens-before). Dicha relación es simplemente una garantía de que las escrituras de memoria realizadas por una declaración específica, son visibles para otra declaración específica. Para visualizar mejor esto, considere lo siguiente:

int contador = 0;
. . . 
Hilo A: contador++;
. . . 
Hilo B: System.out.println(contador);

    Suponga que dicho contador es compartido por dos hilos A y B, y suponga también que A incrementa el contador y que poco después B imprime en la salida estándar el contador. El valor impreso por B puede ser 0 o 1, es decir: no hay garantía de que el cambio a contador por parte del hilo A sea visible para el hilo B, a menos de que el programador establezca una relación happens-before entre estas dos sentencias. La sincronización es una de las formas de crear este tipo de relación.

    Java proporciona dos formas de sincronización:

  1. Métodos sincronizados.
  2. Sentencias sincronizadas.

     Para que un método sea sincronizado, basta con añadir la palabra reservada synchronized a su definición, tal y como se muestra en el Ejemplo ContadorSincronizado. El hacer que los métodos sean sincronizados, tiene dos efectos para las instancias de la clase:

  1. No es posible el traslape para dos invocaciones de métodos sincronizados que se realicen sobre el mismo objeto. Cuando un hilo está ejecutando un método sincronizado para un objeto determinado, todos los demás hilos que invoquen métodos sincronizados para el mismo objeto se bloquean, es decir, se suspende su ejecución, hasta que el primer hilo haya terminado.
  2. Cuando un método sincronizado termina, automáticamente establece una relación happens-before para cualquier invocación subsecuente de un método sincronizado para el mismo objeto, lo que garantiza que los cambios al estado del objeto sean visibles para todos los hilos. 

    Los constructores no pueden ser sincronizados y no tendría sentido, porque sólo el hilo que crea el objeto debería tener acceso a él. De hecho esta situación se identifica como un error de sintaxis.

    Los métodos sincronizados habilitan una estrategia simple para prevenir la interferencia de hilos y los errores de consistencia de memoria. Como regla general podría decirse que: si un objeto es visible por más de un hilo, todas las lecturas o escrituras a las variables (atributos) del objeto deberían ser por métodos sincronizados, con excepción quizá de los atributos final (los cuales no pueden ser modificados una vez que el objeto ha sido construido).

    El Ejemplo PruebaContadorSincronizado muestra cómo podría utilizarse la clase ContadorSincronizado. Analice ambas clases y compárese con sus contra partes de la sección anterior.

Candados intrínsecos y sentencias sincronizadas.

    La sincronización se construye al rededor de una entidad conocida como candado intrínseco o candado de monitor (o simplemente monitor) misma que juega un rol fundamental en dos aspectos relacionados con la sincronización:

  1. Hace cumplir el acceso exclusivo al estado del objeto (exclusión mutua).
  2. Establece y garantiza la relación happens-before esencial para la visibilidad y coherencia (consistencia de memoria).

    Cada objeto tiene asociado un candado intrínseco (lo mismo que cada clase cuando se habla de métodos estáticos). Por convención, si un hilo necesita acceso exclusivo y consistente a los campos de un objeto, tiene que apropiarse de dicho candado antes de hacerlo y liberarlo al terminar.

    Mientras un hilo posea el candado intrínseco, ningún otro hilo puede adquirirlo por lo que se bloqueará. Por otro lado, cuando un hilo libera un candado intrínseco, se establece una relación happens-before entre dicha acción y cualquier adquisición subsecuente del mismo candado.

    Cuando un hilo invoca un método sincronizado, automáticamente adquiere el candado intrínseco para dicho objeto y lo libera cuando el hilo en cuestión termina de ejecutar el método, aun cuando la terminación se dé por una excepción no atrapada.

    En este sentido, sucede que no siempre conviene que todo el método esté sincronizado debido a que se merma la concurrencia (contención de hilos); en algunos casos conviene que se sincronice sólo un bloque de código, debido a que el método podría tener también sentencias que no necesariamente acceden a datos compartidos, o que acceden a datos compartidos distintos, en cuyo caso un enfoque basado en sentencias sincronizadas sería entonces más apropiado (consulte el Ejercicio 8 de los Ejercicios selectos).

    Aunque para el ejemplo que se ha venido desarrollando en esta entrada no tendría mucho sentido utilizar las sentencias sincronizadas, con la finalidad de mostrar su uso y equivalencia para este caso particular, se propone al lector la revisión, comparación y análisis del Ejemplo ContadorSincronizado2 así como su correspondiente clase de prueba del Ejemplo PruebaContadorSincronizado2.

Conclusión.


    Como conclusión general e
s importante resaltar la importancia de la sincronización al trabajar con hilos y recursos compartidos por éstos.

    De los ejemplos comentados es importante también enfatizar y no perder de vista que una clase tan simple como Contador, utilizada en un contexto de hilos compitiendo por acceso a los datos puede derivar, sin la debida sincronización, en un potencial desastre respecto a la consistencia de la memoria y la coherencia de los datos derivada de la interferencia de hilos.

    Por otro lado, conviene también el tener presente que un abuso de la sincronización, pueden incurrir en la nada deseable contención de hilos, haciendo que las potenciales ventajas de la concurrencia no sólo desaparezcan sino que sean absurdas.

    Finalmente aquí, como en otras tantas instancias, se manifiestan dos de esas importantes y perennes lecciones de vida:

  1. "Las nuevas soluciones traen consigo nuevos problemas".
  2. "Todo viene con su precio".


26 de mayo de 2021

Trabajando con hilos.

Hilos y pausas.

    Una vez que un hilo se crea y la máquina virtual de Java lo integra al entorno de ejecución, el hilo empieza a trabajar. En ocasiones, resulta conveniente que un hilo realice pequeñas pausas, o que se sincronice con otros en el sentido de esperar o verificar si algún otro hilo ha hecho ya su trabajo.

    El Ejemplo MensajesPausados muestra el uso del método sleep para pausar temporalmente la ejecución de un hilo. sleep es un método estático, es decir, no se requiere de un objeto concreto que reciba el mensaje, pero sí el nombre de la clase a la cual pertenece (línea 28). En esencia, el ejemplo imprime línea por línea un hermoso poema con pausas de 4 segundos (4000 milisegundos) entre cada línea. Note que esto es sólo un tiempo aproximado, y no debería considerarse con exactitud bajo ninguna circunstancia. En este sentido, resulta fatal el considerar aspectos de cualquier tipo de sincronización con base en el tiempo: no es buena idea y el azar puede (y seguramente lo hará) jugar en su contra.

    Por otro lado, el Ejemplo MensajesPausados2 muestra como única diferencia respecto del anterior la línea 12, particularmente en lo que se refiere a la excepción InterruptedException. El uso de un método como sleep podría generar un excepción del tipo verificada (Checked Exception), por lo que es preciso que se maneje o al menos se atrape. En el primer ejemplo main sólo reporta que de ocurrir la relanzará, es decir, no hace ningún manejo ni la atrapa pero al menos la reporta; el ejemplo en turno omite dicha declaración, por lo que ni siquiera compilará y se reportará un mensaje similar al siguiente:

MensajesPausados2.java:28: error: unreported exception InterruptedException; must be caught or declared to be thrown
            Thread.sleep(4000);
                        ^
1 error

se invita al lector a corroborar lo anterior.

    El Ejemplo MensajesPausados3 muestra una alternativa para la ejecución realizando un manejo muy elemental de la excepción correspondiente a través del bloque try-catch (líneas 22-31). Note que desde el punto de vista de la compilación y la ejecución, el Ejemplo MensajesPausados y el Ejemplo MensajesPausados3 son lógicamente equivalentes.

Hilos interactuando.

   El Ejemplo HilosInteractuando consiste en dos hilos:

  1. El de main.
  2. El derivado de CicloMensaje: Hilo (línea 57).

    El primero es el hilo principal main que cada aplicación de Java tiene. El hilo principal crea y pone en ejecución (líneas 56-59) un nuevo hilo a partir de un objeto ejecutable (CicloMensaje) y espera por él para terminar (líneas 63, 67, 68-69 y 74).

    Si el hilo derivado de la clase CicloMensaje se tarda mucho en terminar, el hilo principal lo interrumpe (línea 71).
   
   Como en los ejemplos de la sección anterior, el hilo de CicloMensaje imprime un bello poema línea por línea pero ahora con una pausa de 1 segundo entre ellas. Note el lector que no se está haciendo uso del método sleep, sino del método join de la clase Thread (línea 67); la diferencia es sutil pero importante: join es un método que debe recibir un objeto concreto a través de su respectivo mensaje (invocación) y, por la naturaleza misma de su funcionamiento, también puede generar la excepción InterruptedException. En este sentido, el diálogo entre los objetos podría interpretarse de la siguiente manera:

"El hilo de main envía un mensaje a Hilo (de CicloMensaje) para decirle que lo esperará 1 segundo más para que termine."

    Si el hilo de CicloMensaje sigue vivo (líneas 63 y 68) y el tiempo transcurrido excede la paciencia preestablecida (líneas 42 ó 48), entonces Hilo es interrumpido (línea 71) antes de que haya impreso todos sus mensajes e imprime un mensaje de reproche antes de salir (línea 34).

Consideraciones.

    El Ejemplo Ejemplo MensajesPausados ilustra dos conceptos importantes:

  1. Interrupciones que pueden presentarse al trabajar con hilos (InterruptedException).
  2. Las pausas de los hilos basadas en tiempo (método sleep( ) de la clase Thread).

    El método sleep hace que el hilo actual interrumpa su ejecución (se duerma) por un tiempo aproximado de cuatro segundos. Es importante que el lector tome en cuenta esto último y nunca estará de más el repetirlo, ya que es un error suponer una exactitud en el tiempo de interrupción debido a que existen gastos de gestión en la administración de los hilos (overhead). Aún peor que lo anterior, es el tratar de sincronizar hilos con base en este criterio de tiempo.

    La sincronización de hilos es una labor no trivial y no debe ser minimizada. Más adelante en el blog se muestran las bases de la sincronización en la entrada Sincronización, misma que proporciona al lector una aproximación un poco más detallada pero finalmente introductoria al respecto.

    El uso del método sleep conlleva la potencial generación de la excepción InterruptedException, la cual es una excepción verificada, es decir, debe ser atrapada o relanzada para que el programa compile y funcione, razón por la cual la línea 12 del Ejemplo Ejemplo MensajesPausados adopta este último mecanismo. Para obtener más información con respecto a los aspectos mencionados en este párrafo, consulte en el Contenido temático del blog más ampliamente los conceptos relacionados con las excepciones.

    Finalmente se conmina encarecidamente al lector a que consulte el API para ampliar y complementar la información respecto a los métodos utilizados en los ejemplos.


17 de diciembre de 2020

Creación de hilos.

   En Java hay básicamente dos formas de generar un nuevo hilo de ejecución:

  1. Crear una instancia de la clase Thread.
  2. Crear una instancia de una clase que sea ejecutable, es decir, una clase que implemente la interfaz Runnable.

   En esta entrada se presentarán dos ejemplos de cada una de estas formas.

Clase Thread.
   El Ejemplo HolaHilo muestra la creación de un hilo cuando una clase (HolaHilo) hereda de la clase Thread (línea 8).

   Todas las subclases de Thread deberían sobreescribir el método run( ), ya que la definición de dicho método establece el comportamiento que tendrá el hilo creado, es decir, el conjunto de acciones a realizar por el hilo, son definidas dentro de este método (líneas 10-12) que, para este caso, consiste únicamente en imprimir un mensaje en la salida estándar.

   Observe cómo en el método main (líneas 14-17) se crea, en la línea 15 un nuevo hilo de ejecución (hilo), mismo que se hecha a andar en la línea 16 a través del método start( ).

   El método start( ) hace que el hilo receptor del mensaje inicie su ejecución, dentro de esta inicialización del hilo ocurren distintos aspectos de gestión que la máquina virtual de Java tiene que realizar para que las cosas funcionen, como la invocación al método run( ) del hilo correspondiente.

   Es importante hacer notar al lector que nunca hay un llamado explícito al método run( ), sino que ocurre un llamado implícito como parte de las gestiones y acciones que realiza el método start( ).

   Por último, el Ejemplo HolaHilo2 muestra una variación del ejemplo anterior.

   En las líneas 11 y 19 se ha hecho uso del método currentThread( ) de la clase Thread, el cuál regresa la instancia del hilo que se está ejecutando (hilo actual), esto con la finalidad de que el método getName( ) obtenga el nombre asignado a dicha instancia. Esta propiedad del hilo en cuestión es utilizada para imprimirla en la salida estándar en las líneas 12 y 20 respectivamente.

   Finalmente, la línea 17 (comentada) utiliza el método setName( ) para asignarle un nombre al hilo ("Hilo Jr."). Se deja como ejercicio para el lector descomentar dicha línea, recompilar, ejecutar y comparar las salidas de los ejemplos correspondientes y comprender la diferencia.

Interfaz Runnable.
   El Ejemplo HolaEjecutable muestra la creación de un hilo utilizando la interfaz Runnable (línea 8).

   Una interfaz en Java es básicamente un contrato que la clase que la implementa debe cumplir, es decir, la clase que implementa una interfaz se compromete a definir todos los métodos que establezca dicha interfaz.

   Para el caso de la interfaz Runnable, el único método que hay que definir es el método run( ) (líneas 10-12), cuya única misión es imprimir un mensaje en la salida estándar.

   En la línea 15 se crea una instancia de la clase HolaEjecutable (ejecutable), misma que es utilizada como argumento para el constructor del hilo creado en la línea 16.

   Si todo sale bien, en la línea 17 existen dos hilos de ejecución:

  1. El de main.
  2. El que se creo en la línea 16 (hilo).

   Note aquí también que nunca hay un llamado explícito al método run( ), sino que más bien hay un llamado implícito en alguna parte del método start( ) (línea 17), el cuál es el encargado de realizar la gestión necesaria para que el hilo recién creado se introduzca en el entorno de ejecución de la máquina virtual de Java. Se recomienda ampliamente al lector el consultar el API para obtener más detalles acerca de la clase Thread y de la interfaz Runnable.

   Finalmente el Ejemplo HolaEjecutable2 muestra una variación del ejemplo descrito con anterioridad. Note particularmente las líneas 11 y 20 en donde se ha hecho uso del método currentThread( ) de la clase Thread, el cuál regresa la instancia del hilo que se está ejecutando (hilo actual); por otro lado, el método getName( ) obtiene el nombre asignado a dicha instancia. Esta propiedad del hilo en cuestión es utilizada para imprimirla en la salida estándar en las líneas 12 y 21 respectivamente.

El intercambio de la líneas 17 y 18 para que mutuamente se excluyan se deja como ejercicio para el lector.


15 de diciembre de 2020

Hilos.

   En un enfoque tradicional de programación, las sentencias y expresiones se ejecutan de manera secuencial, en la programación concurrente, este mismo grupo de sentencias y expresiones se ejecutan de manera concurrente, entendiéndose por concurrencia a la capacidad de las diferentes partes o unidades de un programa para ejecutarse fuera de orden o en orden parcial sin afectar el resultado final.

   Abusando de la simplificación, puede entenderse a la concurrencia como la capacidad de un software o equipo de cómputo para realizar más de una tarea al mismo tiempo.

   En la programación concurrente existen dos unidades básicas de ejecución: procesos e hilos. En el lenguaje de programación Java la programación concurrente se basa principalmente en hilos (threads) aunque estos están estrechamente relacionados con los procesos.

    Los detalles y aspectos relacionados con los procesos gestionados por un sistema operativo quedan fuera de los alcances de este blog; sin embargo, es preciso que el lector tenga al menos una idea general de los aspectos inherentes relacionados con los procesos, razón por la cual se esbozarán algunos conceptos.

   Un proceso es una instancia en ejecución de un programa. En este sentido, un proceso es un entorno de ejecución autocontenido y habitualmente tiene un conjunto de recursos completos, privados, básicos y fundamentales relacionados con su ejecución; así, cada proceso tiene además su propio espacio de memoria.

   Los hilos son también llamados procesos ligeros. Tanto los hilos como los procesos proveen un entorno de ejecución sin embargo, la creación de un nuevo hilo requiere en lo general de menos recursos que la creación de un nuevo proceso.

Programación con hilos.

   Los hilos habitualmente existen dentro de un proceso. Cada proceso tiene al menos un hilo denominado hilo principal. Los hilos comparten los recursos asignados al proceso, tales como la memoria y los archivos asociados por ejemplo, lo cual los hace muy eficientes pero potencialmente problemáticos.

   Desde el punto de vista de los programadores de aplicaciones Java, todo inicia con un solo hilo: main, y este hilo tiene la capacidad de crear nuevos hilos y a partir de ahí generar una programación concurrente a través de dichos hilos.

   Cada hilo en Java está asociado con una instancia de la clase Thread por lo que se recomienda ampliamente al lector revisar la especificación de dicha clase en el API y todo el apartado relacionado con la concurrencia de los tutoriales de Java.

By Hooman Mallahzadeh - Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=100534098


10 de julio de 2017

Ventajas de las excepciones.

   Pueden mencionarse esencialmente tres ventajas principales en el uso de excepciones (vea Advantages of Exceptions):
  1. Separación del código "regular" del de manejo de errores.
  2. Propagación de errores hacia la pila de métodos.
  3. Agrupación y diferenciación de tipos de errores.
Separación del código "regular" del de manejo de errores.
   Las excepciones proporcionan un mecanismo para separar los detalles de la lógica principal del programa, respecto a lo que hay que hacer cuando ocurre algo fuera de lo ordinario.

   Considere el siguiente pseudocódigo para leer un archivo completo en la memoria:

          readFile {
              open the file;
              determine its size;
              allocate that much memory;
              read the file into memory;
              close the file;
          }

   El flujo principal parece bastante simple pero (las cosas a veces salen mal):
  • ¿Qué pasa si el archivo no se puede abrir?
  • ¿Qué pasa si su longitud no puede ser determinada?
  • ¿Qué pasa si no se puede obtener memoria suficiente para contenerlo?
  • ¿Qué pasa si falla su lectura?
  • ¿Qué pasa si el archivo no se puede cerrar?
   Para manejar dichas situaciones, se debe introducir más código para la detección y manejo correspondiente utilizando técnicas tradicionales:

          errorCodeType readFile {
              initialize errorCode = 0;
    
              open the file;
              if (theFileIsOpen) {
                  determine the length of the file;
                  if (gotTheFileLength) {
                      allocate that much memory;
                      if (gotEnoughMemory) {
                          read the file into memory;
                          if (readFailed){
                              errorCode = -1;
                          }
                      } else
                          errorCode = -2;
                  } else
                      errorCode = -3;
                  close the file;
                  if (theFileDidntClose && errorCode == 0)
                      errorCode = -4;
                  else
                      errorCode = errorCode and -4;
              else
                  errorCode = -5;
              return errorCode;
          }

   Las excepciones no ahorran el trabajo del manejo de errores, pero permiten mantener el flujo principal del programa limpio y realizar dicho manejo de errores en otra parte:

          readFile {
              try {
                     open the file;
                     determine its size;
                     allocate that much memory;
                     read the file into memory;
                     close the file;
              } catch (fileOpenFailed) {
                     doSomething;
              } catch (sizeDeterminationFailed) {
                      doSomething;
              } catch (memoryAllocationFailed) {
                      doSomething;
              } catch (readFailed) {
                      doSomething;
              } catch (fileCloseFailed) {
                      doSomething;
              }
          }

Propagación de errores hacia la pila de métodos.
   La segunda ventaja es la capacidad de propagar un error. Considere lo siguiente y suponga que method1 es el único interesado en los errores que pudieran ocurrir dentro de readFile:

          method1 {
              call method2;
          }

          method2 {
              call method3;
          }

          method3 {
              call readFile;
          }

   Para la detección y manejo correspondiente utilizando técnicas tradicionales:

          method1 {
              errorCodeType error;
              error = call method2;
              if (error)
                  doErrorProcessing;
              else
                  proceed;
          }

          errorCodeType method2 {
              errorCodeType error;
              error = call method3;
              if (error)
                  return error;
              else
                  proceed;
          }

          errorCodeType method3 {
              errorCodeType error;
              error = call readFile;
              if (error)
                  return error;
              else
                  proceed;
          }

  Por el mecanismo que tiene la máquina virtual de Java de buscar en la pila de métodos uno que esté interesado en realizar el manejo de una excepción en particular, sólo los métodos interesados se preocupan del manejo de errores:

          method1 {
              try {
                  call method2;
              } catch (exception e) {
                  doErrorProcessing;
              }
          }

          method2 throws exception {
              call method3;
          }

          method3 throws exception {
              call readFile;
          }

Agrupación y diferenciación de tipos de errores.
   Debido a que todas las excepciones lanzadas por un programa son objetos, la agrupación y caracterización en categorías de excepciones es un resultado natural de la jerarquía de clases.

   Un método puede considerar un manejador específico para una excepción determinada:

          catch (FileNotFoundException e) {
                 ...
          }

o bien puede atrapar una excepción basándose en su grupo o tipo general es decir, alguna de sus super clases en la línea de generalización / herencia:

          catch (IOException e) {
                 ...
          }

en este sentido, se pueden encontrar los detalles particulares de lo que ocurrió a través de una consulta al argumento enviado al manejador de excepción:

          catch (IOException e) {
                 // Output goes to System.err.
                 e.printStackTrace();
                 // Send trace to stdout.
                 e.printStackTrace(System.out);
          }

incluso se puede hacer un manejador de excepciones que gestione cualquier excepción:

          // A (too) general exception handler
          catch (Exception e) {
                 ...
          }

   En resumen, se pueden crear grupos de excepciones y darles un manejo general, o se pueden utilizar categorías especificas de excepciones para diferenciarlas y manejarlas de una manera más precisa.


Lanzado y encadenamiento.

Lanzar una excepción.
    Antes de poder atrapar una excepción, debe haber en alguna parte un código que cree y lance una. Cualquier código puede lanzar una excepción, pero independientemente de qué o quién lance una excepción siempre es lanzada con la sentencia o cláusula throw.

    El Ejemplo Excepciones muestra lo anterior. Se tienen básicamente dos métodos además de main, uno de ellos (líneas 9-23) crea y lanza una excepción (línea 12) y el otro no (líneas 25-35). Note cómo la excepción lanzada en la línea 12 es atrapada en la línea 13 y relanzada en la línea 16 para que a su vez vuelva a ser atrapada en la línea 40. Es importante que el lector comprenda también los comentarios de las líneas 17 y 22, mismos que se dejan como ejercicio para que se validen y verifiquen.

    En este mismo orden de ideas, resulta conveniente que el lector revise el Ejemplo descrito en la entrada Implementación del ADT Racional en donde también se crea y lanza una excepción en un contexto particular.

   Adicionalmente a lo anterior, se recomienda también revisar la sección Excepciones de la entrada Ejemplos selectos de transición y la de Pila primitiva de la entrada Pilas (implementación), en las que encontrará respectivamente, la jerarquía de clases relacionadas con las excepciones y un ejemplo de creación de una excepción definida por el programador.

Excepciones encadenadas.
   Frecuentemente una aplicación responde a una excepción lanzando otra excepción, dicho de otra forma: la primera excepción causa la segunda. En este sentido puede ser muy útil el saber cuando una excepción causa otra. Las excepciones encadenadas ayudan al programador a realizar ésto.

   Los siguientes son una lista de métodos y constructores de la clase Throwable que dan soporte a las excepciones encadenadas:
          Throwable getCause( )
          Throwable initCause(Throwable)
          Throwable(String, Throwable)
          Throwable(Throwable)

   El argumento Throwable en initCause y en los constructores es la excepción que causó la excepción actual. getCause regresa la excepción que causó la excepción actual e initCause establece la causa de la excepción actual.

   El siguiente fragmento de código muestra un esqueleto de cómo utilizar una excepción encadenada:
           try {
                       . . .

           } catch (IOException e) {
                  throw new SampleException("Other IOException", e);
           }

    En el Ejemplo Excepciones2 se muestra un ejemplo de lanzado y encadenamiento que está en directa relación y continuación con lo expuesto en la sección anterior. Note particularmente las líneas 19 y 20, en donde se crea respectivamente la nueva excepción a partir de otra, y se incorpora al entorno de ejecución de la máquina virtual de Java (relanza). Asegúrese de comprender en su totalidad el ejemplo y de compararlo con el de la sección anterior (Ejemplo Excepciones). 

    Finalmente considere el Ejemplo Excepciones3. Aquí se tiene lo que se conoce como una pila de invocación de métodos, en donde el último en ser llamado es el que provoca o genera la excepción (creaExcepcion( )). Note cómo todos los métodos indican que pueden lanzar una excepción (throws Exception) pero que ninguno de ellos, exceptuando main, hace una manejo o atrapa alguna: todos la dejan pasar. El nivel de profundidad en la pila de invocación de métodos puede ser mayor o menor que el mostrado en el ejemplo en cuestión, aquí lo importante es asegurarse que se comprende el funcionamiento general, el relanzado implícito, y el encadenamiento de una sola excepción transferida entre los métodos involucrados.


6 de julio de 2017

Excepciones.

   El lenguaje de programación Java utiliza excepciones para manejar errores y otros eventos excepcionales. Un programa en Java puede utilizar excepciones para indicar que ha ocurrido un evento excepcional, de hecho, el término excepción es la forma corta de decir evento excepcional.

   En este sentido, una excepción es un evento que ocurre durante la ejecución de un programa en Java, que interrumpe el flujo normal de ejecución del programa (vea What is an Exception).

   Cuando un error ocurre dentro de un método, el método crea un objeto (objeto de excepción) y lo coloca en el entorno de ejecución, el cual contiene información acerca del error, incluyendo su tipo y el estado del programa cuando ocurrió el error. A éste proceso se le denomina lanzar una excepción.

   Después de que un método lanza una excepción el entorno de ejecución intenta encontrar a alguien (manejador de excepción) que la atrape y la maneje:

La pila de invocación de métodos y la búsqueda del manejador de excepción (adaptada de What is an Exception).

   Para lanzar un excepción se debe hacer uso de la cláusula throw adjunta a un objeto de excepción (descendiente de la clase Throwable) para proporcionar información específica acerca del evento excepcional que ocurrió.

   Un código válido en el lenguaje de programación Java debe respetar la cláusula catch (también llamado requisito de especificación), lo cual significa que el código que podría lanzar ciertas excepciones debe estar encerrado por alguna de las siguientes:
  • Una sentencia try que atrapa la excepción.
  • Un método que especifica que puede lanzar la excepción.
  Cabe mencionar que no todas las excepciones están sujetas a esta situación, y la razón es que existen tres categorías básicas de excepciones y sólo una de ellas está sujeta a dicho requisito (excepción comprobada):
  1. Excepción comprobada (checked exception): condiciones excepcionales que un programa o aplicación bien escrita debe considerar.
  2. Error: condiciones excepcionales que son externas al programa o la aplicación; usualmente no es posible anticiparlas o recuperarse de ellas (mal funcionamiento del hardware o del sistema).
  3. Excepción en tiempo de ejecución (runtime exception): condiciones excepcionales que son internas al programa o la aplicación sin que la aplicación pueda anticiparlas o recuperarse de ellas (errores lógicos a bugs).
   Las excepciones de error y de tiempo de ejecución se conocen comúnmente como excepciones no comprobadas o verificadas (unchecked exceptions).

   Un programa puede atrapar excepciones por medio de la combinación de los bloques try, catch y finally:
  1. El bloque try identifica un bloque de código en el que una excepción puede ocurrir.
  2. El bloque catch identifica un bloque de código, conocido como manejador de excepción (exception handler), que puede atrapar y manejar un tipo específico de excepción.
  3. El bloque finally identifica un bloque de código que en general se garantiza que se ejecutará independientemente de si se generó (catch) o no (try) la excepción, por lo que es el lugar preciso para cerrar archivos, liberar recursos y cualquier tarea de limpieza que se requiera como parte de la ejecución del código que se ejecutó en el try.

   Una cláusula try podría contener al menos un bloque catch o finally, y múltiples bloques catch.

    Como un primer acercamiento, considere inicialmente el Ejemplo DivisionSinManejoExcepcion el cual muestra la posible generación de una excepción no verificada (unchecked) si se intenta hacer una división por cero (línea 16). La excepción que podría generarse es de la clase ArithmeticException; pero todavía más: ¿Qué sucede si en lugar de un número se introduce una letra o una cadena? Se conmina al lector a probar lo hasta aquí expuesto.

    El Ejemplo DivisionConManejoExepcion toma ya en consideración lo expuesto en el párrafo anterior e ilustra el uso de la cláusula try-catch líneas (27-43). El manejo de excepciones consiste en hacer que un programa, ante una situación anormal o fuera del flujo esperado de ejecución, pueda recuperarse y continuar y no simplemente terminar. Note que con excepción de las cláusula en cuestión, el programa es esencialmente el mismo que el anterior, con la consideración del ajuste de una bandera para determinar o no la continuación del programa (líneas 24, 35 y 44). Este ejemplo, además del manejo de la excepción aritmética, también considera la excepción InputMismatchException para el caso de que no se proporcionen los números enteros como entrada sino alguna otra cosa.

    ¿En qué orden debería colocarse las excepciones?, ¿cuál debemos poner primero? Las recomendaciones, una vez que se asume que se han localizado las excepciones más pertinentes, serían dos:

  1. Atrape primero la excepción que a su consideración tenga más probabilidad de ocurrir, después la segunda y así sucesivamente.
  2. Coloque primero las excepciones más específicas y al final las más generales; para ello, deberá consultar la jerarquía de clases correspondiente.

   Finalmente, además de invitar al lector a revisar el API de Java para la revisión y familiarización de las excepciones comentadas en esta entrada, es importante resaltar que la decisión de utilizar excepciones propias comprobadas (checked) o no (unchecked) debería seguir esta guía: si un cliente o usuario de nuestras clases tiene una expectativa de recuperarse razonablemente de una excepción, entonces la excepción debería ser del tipo comprobada (checked); por otro lado, si no puede hacerse nada para recuperarse de la excepción ésta debería ser no comprobada (unchecked). Para más información vea Unchecked Exceptions - The controversy.