1

Uso de hilos en Java(parte 7)

Anteriormente hemos visto:

  • Ejecución secuencial.

  • Uso de concurrencia heredando de la clase Thread e implementar la interfaz Runnable.

  • Particionamiento del trabajo entre los hilos.

  • Comparación del tiempo de la ejecución secuencial vs la ejecución paralela.

  • Ejecutores : Explicación, tipos, ventajas y desventajas.

  • Ejemplo de uso de ejecutores.

  • Entrelazado de instrucciones, sus consecuencias y código de ejemplo de entrelazado.

  • Solución e implementación de la solución al problema.

  • Explicación de synchronized, relación con los cerrojos y ejemplo de cerrojos.

  • Tipos de datos atómicos.

  • Control de acceso de hilos usando semáforos.

  • Control de sincronización a través del uso de barreras.

Ahora veremos:

  • Autómatas celulares 1D.

  • Tratamiento de imágenes(2D) con hilos.

Años atrás varios autores habían publicado estudios sobre distintas simulaciones de comportamiento de propagación de incendios, expansión de células tumorales, transferencia de calor y muchas más, todas estas simulaciones están basadas en modelos matemáticos y ayudan de manera significativa a ver como se desarrollan estas situaciones, estos modelos se llaman autómatas celulares, estas simulaciones se consiguen a través de la generación de nuevos valores que irán acorde a la expresión matemática que define el modelo, los autómatas celulares se componen de:

  • Un espacio regular: Las dimensiones en las que existirá autómata en cuestión, si se tratará de un array seria 1D, si se tratará de una matriz seria 2D, y asi sucesivamente.
  • Un conjunto de celdas o células: Son los valores que componen al estado del autómata. Si estuviéramos con uno 1D entonces una célula sería una posición del array.
  • Conjunto de estados: Los diferentes valores que puede tener cada elementos que componen nuestro autómata, esto define el espacio del dominio.
  • Configuración Inicial: Es la asignación inicial de un estado a cada célula componente.
  • Vecindades: Define las células que serán adyacentes a una célula dada, intervendrán en la generación de nuevos estados ya que participan como factores en la expresión matemática.
  • Función de Transición: Es la expresión matemática que se utilizará para generar nuestros estados de las células, esta función utilizará los estados actuales de la célula dada y sus vecinas a la hora del cálculo.
  • Condición de frontera: Define que valores usaremos en el caso de que una célula esté en el límite de nuestro conjunto y alguna de sus vecinas exceda este límite, veremos una explicación con más claridad en el siguiente ejemplo.

Con esto ya podemos definir de forma muy coloquial un autómata celular, voy a programar un autómata que vendrá definido de la siguiente manera:

  • Espacio regular: Será 1D ya que usaré un array de 10 elementos.
  • Un conjunto de celdas o células: Los 10 elementos del array.
  • Conjunto de estados: Serán número no negativos que serán menores que 3 => [0,3).
  • Configuración Inicial: Serán números generados por la función random() de Java.
  • Vecindad: Serán las células anterior y siguiente de una célula_i.
  • Función de Transición: NuevoValor = (ValorActual + VecinaIzq + VecinaDerecha) % 3.
  • Condición de frontera: Imagina que estamos tratando la posición 0 del vector, si tomo la celúla anterior estaría accediendo a las posición -1 lo cual no es posible en Java(mentira, con el módulo se puede) pero en Python si, por lo que en este caso tomaré la célula siguiente multiplicada por 2, en el caso de que estemos tratando la última posición entonces haré lo mismo usando la célula anterior * 2.

Este sería el código:

public class Automata1D {
  static int numCelulas = 10;// Obtener un conjunto inicial generado aleatoriamente.
  public static int[] configuracionInicial() {
    int configInicial[] = new int[numCelulas];for (int i = 0; i < numCelulas; i++) {
      configInicial[i] = ((int) (Math.random() * numCelulas)) % 3;
    }
    return configInicial;
  }

  // A partir de una configuracionActual obtendremos una nueva.
  public static int[] siguienteGeneracion(int[] configuracionActual) {
    int nuevaConfiguracion[] = new int[numCelulas]; // Es necesario utilizar un array auxiliar.for (int i = 0; i < numCelulas; i++) {if (i == 0) { // En esta caso no podemos tomar la vecina izq. ya que nos salimos del array y// genera una excepcion. En este caso solo contaré con una vecina * 2.int vecinaDer = configuracionActual[i + 1];
        nuevaConfiguracion[i] = (configuracionActual[i] + vecinaDer * 2) % 3; // Aplicamos la formula.
      } else {
        if (i == numCelulas - 1) { // Si estamos en la última posición del arrray, entonces no contaré con la// vecina derecha.int vecinaIzq = configuracionActual[i - 1];
          nuevaConfiguracion[i] = (configuracionActual[i] + vecinaIzq * 2) % 3;
        } else {
          int vecinaIzq = configuracionActual[i - 1];int vecinaDer = configuracionActual[i + 1];
          nuevaConfiguracion[i] = (configuracionActual[i] + vecinaIzq + vecinaDer) % 3;
        }
      }
    }
    return nuevaConfiguracion;
  }

  public static void main(String[] args) {
    int inicial[] = configuracionInicial();System.out.println("La configuracion inicial(T) es:");
    imprimirConfiguracion(inicial);int siguiente[] = siguienteGeneracion(inicial);System.out.println("La configuracion (T + 1) es:");
    imprimirConfiguracion(siguiente);System.out.println("La configuracion (T + 2) es:");
    imprimirConfiguracion(siguienteGeneracion(siguiente));
  }

  public static void imprimirConfiguracion(int[] configuracion) {
    for (int i = 0; i < numCelulas; i++) {System.out.print(configuracion[i] + " ");
    }
    System.out.println("");
  }
}

Y este sería el resultado de la ejecución:
automata.png

Con esto ya tenemos un ejemplo de un autómata 1D, ahora veremos uno 2D usando el ejemplo de tratamiento de imágenes.

Para hablar de tratamiento de imágenes definiré unos conceptos de forma coloquial:

  • Tratamiento de una imagen : Aplicar diferentes operaciones(definidas por una fórmula matemática) sobre una imagen.
  • Imagen : Es una matriz de píxeles que definirán una imagen.
  • Píxel : Unidad de información que compone las imágenes, cada píxel tendrá un valor asociado que definirá un color(en el caso de imágenes con color) o una escala de gris(en caso de imágenes de blanco y negro).

Existen diversos procesos que podemos realizar para modificar imágenes como la operación de suavizado, esta viene definida por la siguiente fórmula matemática:
formula.png
Esta fórmula indica que el nuevo valor del PIXEL_i-j se calcula a partir de (4 * su valor actual + los valores de los píxeles vecinos) / 8 con esto obtenemos un píxel suavizado, en cierto modo es parecido a aplicar una media, veamos una definición de este autómata:

  • Espacio regular: Será 2D ya que se trata de una matriz(1000x1000) que define una imagen.
  • Un conjunto de celdas o células: Los 1000*1000 píxeles.
  • Conjunto de estados: Serán número no negativos en escala de grises siendo 0 negro y 1 blanco=> [0,1].
  • Configuración Inicial: Serán números generados por la función random() de Java.
  • Vecindad: Serán las células que están a la izquierda, derecha, arriba y abajo de una célula_i.
  • Función de Transición: La adjuntada en la foto anterior.
  • Condición de frontera: Cuando estemos en los límites de la imagen los píxeles usaremos el módulo y una suma para salir de los límites y evitar la generación de excepciones.

Os adjunto el código del suavizado procesado por una clase que hereda de Thread, cada hilo procesaran secciones distintas de la imagen(inicio, fin en el constructor de la clase):

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import javax.swing.JFrame;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import jdk.internal.org.objectweb.asm.tree.analysis.Frame;

public class Suavizado extends Thread {
  static BufferedImage imagenInicial;
  static BufferedImage imagenFinal;
  static int dimension = 1000; // Número de filas y columnas de la imagen.int inicio, fin; // Con estos atributos definimos las secciones que tratara cada hilo.

  public Suavizado(int i, int f) {
    this.inicio = i;
    this.fin = f;
  }

  public void run() {
    // Declaramos las variables que contendran las posiciones a tomar, los valores y// el nuevo valor.int nuevoValor;int posicionIzq, posicionDer, posicionArriba, posicionAbajo;int vecinaIzq, vecinaDer, vecinaArriba, vecinaAbajo;for (int i = this.inicio; i < this.fin; i++) { // Con este for cada hilo ejecuta su seccionfor (int j = 0; j < dimension; j++) {// Calcular posiciones
        posicionArriba = (j + 1 + dimension) % dimension;
        posicionAbajo = (j - 1 + dimension) % dimension;
        posicionIzq = (i - 1 + dimension) % dimension;
        posicionDer = (i + 1 + dimension) % dimension;// Obtener valores de los vecinos
        vecinaArriba = imagenInicial.getRGB(i, posicionArriba);
        vecinaAbajo = imagenInicial.getRGB(i, posicionAbajo);
        vecinaIzq = imagenInicial.getRGB(i, posicionIzq);
        vecinaDer = imagenInicial.getRGB(i, posicionDer);// Aplicamos la funcion de transicion
        nuevoValor = (4 * imagenInicial.getRGB(i, j) + vecinaAbajo + vecinaArriba + vecinaIzq + vecinaDer) / 8;// Asignamos el valor suavizado.
        imagenFinal.setRGB(i, j, nuevoValor);
      }
    }
  }

  // Este método static crea una número de hilos en función al número de nucleos.
  public static Suavizado[] lanzarHilos() {
    int numeroNucleos = Runtime.getRuntime().availableProcessors();int filasPorHilos = dimension / numeroNucleos;
    Suavizado[] hilos = new Suavizado[numeroNucleos];for (int i = 0; i < numeroNucleos; i++) { // Con este for creamos los hilos asignando las secciones.int inicio = filasPorHilos * i;int fin = filasPorHilos * (i + 1);
      hilos[i] = new Suavizado(inicio, fin);
    }
    for (int i = 0; i < numeroNucleos; i++) { // Lanzamos los hilos
      hilos[i].start();
    }
    return hilos; // Devolvemos los hilos tras haber sido lanzados.
  }

  // Este método static creará la imagen inicial de forma aleatoria.
  public static BufferedImage inicial() {
    // Creamos la imagen y usamos una variable del tipo Color de forma auxiliar.
    BufferedImage imagen = new BufferedImage(dimension, dimension, BufferedImage.TYPE_BYTE_GRAY);
    Colorcolor;for (int i = 0; i < dimension; i++) { // En este bucle asignamos valores aleatorios a la imagen.for (int j = 0; j < dimension; j++) {
        float valorEscala = (float) Math.random();color = new Color(valorEscala, valorEscala, valorEscala);
        imagen.setRGB(i, j, color.getRGB());
      }
    }
    return imagen;
  }

  // En el main creamos la imagen, esperamos a los hilos y mostramos la imagen.
  public static void main(String[] args) throws InterruptedException {
    imagenInicial = inicial();
    imagenFinal = new BufferedImage(dimension, dimension, BufferedImage.TYPE_BYTE_GRAY);
    Suavizado[] hilos = lanzarHilos();for (int i = 0; i < hilos.length; i++) {
      hilos[i].join();
    }
    mostrarImagenes();
  }

  // Este método static usa clases de Java swing para mostrar las imagenes.
  public static void mostrarImagenes() {
    JFrame frameInicial = new JFrame("Visualizador Inicial");
    JFrame frameFinal = new JFrame("Visualizador Final");
    JLabel imagen_inicial = new JLabel(new ImageIcon(imagenInicial));
    JLabel imagen_final = new JLabel(new ImageIcon(imagenFinal));
    frameInicial.getContentPane().add(imagen_inicial);
    frameFinal.getContentPane().add(imagen_final);
    frameInicial.pack();
    frameFinal.pack();
    frameInicial.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    frameFinal.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    frameInicial.setVisible(true);
    frameFinal.setVisible(true);
  }
}

Tras ejecutarlo se crearán dos ventanas(la primera sobre la segunda), obtenemos los siguientes resultados:
visualizacion.png
Espero que os sea de utilidad en alguna ocasión y si tenéis alguna cuestión o consejo para mejorar mis tutoriales por favor enviadme un mensaje, muchas gracias y un saludo 😃.

Escribe tu comentario
+ 2