75-62 Técnicas de Programación Concurrente II 2004 java Threads FIUBA Ing. Osvaldo Clúa Bibliografía: tutorial de Java en http://java.sun.com Un buen libro (algo teórico) es Garg: Concurrent and Distributed Computing in Java Puede haber material que ayude en las páginas de sus clases en http: Se presupone un conocimiento general de Java. En Internet hay bastantes cursos para poder seguir. En la página Web de la materia se dejó una colección de guías de estudio que se usó en años anteriores. Threads Para crear un thread en Java existen dos opciones: • Extender la clase Thread. • Implementar la interface Runnable. La elección de alguna de estas opciones depende del diseño general. Al no admitir Java herencia múltiple, la implementación de interfaces sirve para denotar propiedades secundarias al eje de herencia. El siguiente es un ejemplo de estas dos formas de hacerlo: 1. /** 2. * Primero.java 3. * 4. * El HolaMundo de las threads 5. */ 6. 7. class T1 extends Thread 8. { 9. public void run() { 10. System.out.println 11. ("Hola, soy tu primer Thread, extendi la clase Thread "+ 12. Thread.currentThread()); 13. } 14. } 15. class T2 implements Runnable 16. { 17. public void run() { 18. System.out.println 19. ("Hola, soy tu primer Thread, implemente Runnable " + 20. Thread.currentThread()); FIUBA 2004 756-62 Técnicas de Programación Concurrente II 21. } 22. } 23. 24. public class Primero 25. { 26. public static void main(String args[]) { 27. T1 t1 = new T1(); 28. T2 ru = new T2(); 29. t1.start(); 30. Thread t2 = new Thread (ru); 31. t2.start(); 32. System.out.println("Hola, soy el Thread principal"+ 33. Thread.currentThread()); 34. try{ 35. t1.join(); 36. t2.join(); 37. } 38. catch(InterruptedException e){ 39. e.printStackTrace(); 40. System.exit(0); 41. } 42. System.out.println("Chau, solo queda el Thread principal"+ 43. Thread.currentThread()); 44. } 45. } En el código anterior se ven las dos formas de crear un thread. Al llamar al método start() de Thread se crea efectivamente el Thread y se le da control al método run() que es el que hay que programar. En el caso de implementar runnable se debe crear el thread para que haya acceso a este método. En la clase Thread están definios los métodos start() y join() con el significado habitual. La salida de este programa se ve así: 1.Hola, soy tu primer Thread, extendi la clase Thread Thread[Thread-0,5,main] 2.Hola, soy el Thread principalThread[main,5,main] 3.Hola, soy tu primer Thread, implemente Runnable Thread[Thread-1,5,main] 4.Chau, solo queda el Thread principalThread [main,5,main] -2- FIUBA 2004 756-62 Técnicas de Programación Concurrente II Donde: Thread-0, Thread-1 y principalThread son los nombres de los threads. 5 es la prioridad y main es el grupo al que pertenecen. Estados de un thread: Un thread Java puede estar en uno de cuatro estados posibles: • New. Cuando el objeto se crea (usando la isntrucción new()). • Runnable. Una vez llamado el método start(). • Blocked. Cuando se está a la espera de una operación de I/O o se llamó a los métodos sleep(), suspend() (este último está desaprobado y se quitará de alguna versión posterior de Java) • Dead.. Cuando se terminó el run(...) o se llamó a stop() (también desaprobado). Como ejemplo mas completo se propone un conjunto de dos contadores sobre un mismo canvas: 1. /* El contador */ 2. import java.awt.*; 3. 4. public class Cont implements Runnable 5. { 6. long cont=0;int aDormir; 7. Color color; 8. 9. Cont(int tiempo, Color c){ 10. color=c; aDormir=tiempo; 11. } 12.public void run() { 13. while (true){ 14. try {Thread.sleep(aDormir);} 15. catch (InterruptedException e) {} 16. cont++; 17. } -3- FIUBA 2004 756-62 Técnicas de Programación Concurrente II 18. } 19.public void paint(Graphics g) { 20. g.setColor(color); 21. g.setFont(new Font (null,Font.PLAIN,24)); 22. g.drawString( String.valueOf(cont), 10, 30); 23. } 24.} Este contador duerme según un parámetro de construcción (tiempo) y se incrementa en uno. Además tiene la responsabilidad de dibujarse en paint. Una aplicación usa de este contador, creando uno distinto en un canvas que divide en dos graphics: 1. /* La aplicacion del contador */ 2. import java.awt.*; 3. import java.awt.event.*; 4. 5. class ContAp extends Frame implements Runnable{ 6. Thread tic,tac,anima; 7. Cont c1,c2; 8. Canvas c; 9. int ancho=200,alto=300; 10.ContAp(String s){ 11. super(s); 12. c1=new Cont(500,Color.black); 13. c2=new Cont(1000,Color.blue); 14. tic=new Thread(c1);tac=new Thread(c2); 15. c=new Canvas (); 16. c.setSize(ancho,alto); 17. add(c); pack(); setVisible(true); 18. tic.start();tac.start(); 19. addWindowListener(new WindowAdapter() 20. {public void windowClosing(WindowEvent e) {System.exit(0);}}); 21. anima=new Thread(this);anima.start(); 22. repaint(); 23. } 24.public void paint (Graphics g){ 25. Graphics cg=c.getGraphics(); 26. cg.setColor(Color.LIGHT_GRAY); 27. cg.fillRect(0,0,ancho,alto); 28. Graphics g1=cg.create(0,0,ancho,alto/2);c1.paint (g1); -4- FIUBA 2004 756-62 Técnicas de Programación Concurrente II 29. Graphics g2=cg.create(0,alto/2,ancho,alto/2); c2.paint(g2); 30. } 31.public void run() { 32. while (true) { 33. try {Thread.sleep(250);} 34. catch (InterruptedException e) {} 35. repaint(); 36. } 37. } 38. 39.} En el método paint se ve como divide el graphic en dos y le pide a cada contador que lo dibuje. Falta una clase principal que tenga el método public static void main(String s[]) para completar el ejemplo. Se usaron 3 threads: tick, tack y anima. Las dos primeras son de cada contador en tanto que la última es el thread necesario para la animación. Pruebe sin ella a ver que pasa. En el caso anterior cada elemento tiene su contexto gráfico propio. El ejemplo de los puntos adjunto a esta práctica muestra una variación compartiendo un único canvas. Como último ejemplo simple, se analizará la aplicación de puntos que rebotan en un canvas. Acá los puntos son una clase cuya responsabilidad es dibujarse, mantener su posición y velocidad. La clase PunCan contiene al canvas, la inicialización y el método paint() para dibujarse. En este caso se usó una técnica de animación conocida como Double Buffer. en la clase PunCan se hizo override de update: public void update(Graphics g){paint(g);} Evitando que se borre la ventana con cada llamado. Se definió: Graphics miGr; Image miIm; -5- FIUBA 2004 756-62 Técnicas de Programación Concurrente II Para tener donde trabajar offscreen. La image se crea del mismo tamaño que el canvas: miIm=createImage((new Double(ca.getBounds().getWidth ())).intValue(),(new Double(ca.getBounds().getHeight ())).intValue()); miGr=miIm.getGraphics(); Y de esta imagen se crea un nuevo contexto gráfico. Se dibuja sobre este contexto gráfico y se envía toda la imagen al canvas de la pantalla en un solo paso: for(int i=0;i<cant;i++) {p[i].paint(miGr);} Graphics g1=ca.getGraphics(); g1.drawImage(miIm,0,0,this); } Esto anula el flicker. Es importante que entienda como cambiar la velocidad de animación. Sincronización El siguiente es el código de un productor consumidor "automático" para su uso desde la consola de texto: 1. /** 2. * Buffer.java Con primitivas de sincronizacion 3. */ 4. 5. public class Buffer 6. { 7. private static final int BUFFER_SIZE = 5; 8. private int cant; // items en el buffer 9. private int in; // Proximo libre 10. private int out; // Proximo lleno 11. private Object[] buffer; 13. 14. 15. 16. 17. 18. 19. public Buffer() { cant = 0; // Comienza Vacio in = 0; out = 0; buffer = new Object[BUFFER_SIZE]; } 21. 22. 23. public synchronized void poner(Object item) { while (cant == BUFFER_SIZE) { try {wait();} -6- FIUBA 2004 756-62 Técnicas de Programación Concurrente II 24. catch (InterruptedException e) { } 25. } 26. ++cant; //Agrega al buffer 27. buffer[in] = item; 28. in = (in + 1) % BUFFER_SIZE; 29. if (cant == BUFFER_SIZE) 30. System.out.println("Entro " + item + 31. " Buffer LLENO"); 32. else 33. System.out.println("Entro " + item + 34. " Buffer Size = " + cant); 35. notify(); // despierta algun consumidor 36. } 37. public synchronized Object sacar() { 38. Object item; 39. while (cant == 0) { 40. try {wait(); } 41. catch (InterruptedException e) { } 42. } 43. --cant; 44. item = buffer[out]; 45. out = (out + 1) % BUFFER_SIZE; 46. if (cant == 0) 47. System.out.println("Salio " + item + " Buffer VACIO"); 48. else 49. System.out.println("Salio " + item + 50. " Buffer Size = " + cant); 51. notify(); 52. return item; 53. } 54. } Este buffer es el objeto encargado de hacer la sincronización entre el productor y el consumidor. No es un objeto activo (No tiene threads). En Java cada objeto tiene un lock (monitor es el meta-lenguaje de Java). Al ejecutar un método synchronized se toma este monitor y se lo libera al terminar la ejecución del método. Solo un thread puede tener el monitor de un objeto. El lazo de sincronización está en el uso de: wait() que libera el monitor del objeto y suspende al thread que lo llama hasta que ocurra una interrupción. notify() que provoca una interrupción a algún thread que espera por el monitor del objeto. notifyAll() que provoca una interrupción en todos los threads que esperan por el monitor del objeto. Como pueden interrumpirse (y salir del wait( )) por otras causas, se hace el while -7- FIUBA 2004 756-62 Técnicas de Programación Concurrente II por la condición de las líneas 23 y 40. El productor es: 1. /** 2. * Productor.java 3. */ 5. import java.util.*; 7. public class Productor extends Thread 8. { 9. private Buffer buffer; 10. final static int NAP_TIME=5; 11. public Productor(Buffer b) {buffer = b;} 12. public void run() 13. { 14. Date mens; 15. while (true){ 16. dormir();mens = new Date(); 17. System.out.println("Producido " + mens); 18. buffer.poner(mens); 19. } 20. } 21. public static void dormir() { // para suspenderse 22. int sleepTime = (int) (NAP_TIME * Math.random () ); 23. try { Thread.sleep(sleepTime*1000); } 24. catch(InterruptedException e) { } 25. } 26. } y el consumidor 1. /** 2. * Consumidor.java 3. */ 5. import java.util.*; 7. public class Consumidor extends Thread 8. { 9. private Buffer buffer; 10. final static int NAP_TIME=5; 11. public Consumidor(Buffer b) 12. {buffer = b;} 13. -8- FIUBA 2004 756-62 Técnicas de Programación Concurrente II 14. public void run() 15. {Date mens; 16. while (true){ 17. dormir(); 18. System.out.println("Consumidor quiere consumir."); 19. mens = (Date)buffer.sacar(); 20. } 21. } 22. public static void dormir() { // para suspenderse 23. int sleepTime = (int) (NAP_TIME * Math.random() ); 24. try { Thread.sleep(sleepTime*1000); } 25. catch(InterruptedException e) { } 26. } 27. } Para su ejecución hace falta una clase como la que sigue: 1. /** * Prubuf.java */ 4. public class Prubuf 5. { 6. public static void main(String args[]) { 7. Buffer sr = new Buffer(); 8. Productor pt = new Productor(sr); 9. Consumidor ct = new Consumidor(sr); 10. pt.start(); ct.start(); 11. } 12. } En el archivo zip correspondiente a esta guía encontrará un productor-consumidor con interface gráfica como la que sigue. El código sin embargo no hace uso de un thread continuo como el primer código. Ejercicios: 1. Resolver el Productor Consumidor con un thread para cada uno mas los threads necesarios para la interface gráfica. 2. Generalizarlo para "n" productores y "k" consumidores (a ingresar desde la interface). 3. Resolver con una interface gráfica parecida el problema de los Fumadores. 4. Haga un programa que permita hacer Drag & Drop de una forma simple a través de un canvas. Use las clases asociadas a Java2D para determinar los límites y dibujar las formas (no hay threads). 5. Programe un juego simple con una víbora que se mueve en la pantalla. Ud dispone de una palmeta (rectángulo) para aplastarle la cabeza. Si no lo logra, una marca de la palmeta queda en la pantalla y la víbora debe esquivarla. -9-