Detta dokument förutsätter Netscape 4 eller Internet Explorer 4, om du har tidigare versioner klicka här.

Innehåll, föreläsning 5 (Animering, trådar och strömmar)

Föreläsning 5: Animering, trådar och strömmar

Klassen Thread

De applets vi hittills tittat på har alla varit passiva i den meningen att de inte gjort någonting av sig själva. Vissa applikationer, t.ex. animeringar, arbetar annorlunda; de lever sitt eget liv och tar egna initiativ. För att den typen av applikationer skall kunna leva tillsammans med andra är det mycket viktigt att de inte tar all datorkraft i anspråk. I Java finns stöd för s.k. trådar, alltså möjlighet att exekvera någonting - inom ett program eller en applet - separat i sitt eget kontrollflöde. Man kan t.ex. inte låta metoden paint() sköta en lång animering som består i att visa bild efter bild eftersom datorn under tiden inte kan göra annat. Alla små animeringar som finns på web-sidor skulle då hindra oss från att göra något annat medan de pågår.

En tråds liv

Förutom att placera trådar i de olika tillstånden måste man som programmerare ibland också synkronisera olika trådar som arbetar på samma data.

Klassen Thread implementerar trådar. Dess vanligaste konstruktorer är:

Thread() 
Thread(Runnable)
Runnable är ett gränssnitt med en enda metod, run() som alltså måste implementeras, endera i en subklass till Thread eller i en annan klass som då ges som första argument till den andra konstruktorn. När en tråd startas anropas run() automatiskt. Eftersom Thread implementerar (en tom) run() ska man inte skriva implements Runnable om man subklassar Thread.

Viktiga metoder i Thread är

static void sleep(long millis) - söver ner den tråd som exekverar
static void sleep(long millis , int nanos) - söver ner den tråd som exekverar
void start() - startar exekveringen av tråden
void final stop() - stoppar exekveringen av tråden
void final suspend() - stoppar tillfälligt en tråd
void final resume() - startar om en tråd som stoppats med suspend()
static yield() - stoppar tillfälligt den tråd som nu exekverar
Det kan se konstigt ut med statiska metoder men om de anropas i en tråds metod arbetar de ju på den tråden eftersom det är den som exekverar.

Ingen av metoderna ovan ersätts vanligen, man anropar dem vid behov.

Animering

Vårt första exempel med trådar är en enkel animering. Appleten nedan visar en text som skyms av en oval som ändrar storlek.

Koden följer här:

import java.applet.*;
import java.awt.*;

public class WelcomeAnimation extends Applet implements Runnable
{
    static final int sleepingTime = 100;
    static final String msg = 
	"Welcome to the World of Internet Programming";
    private Font font;
    private int size = 100;
    private boolean shrinking = true;
    private Thread animator = null;

    public void init() 
    {
	super.init();
	font = new Font("Times", Font.BOLD, 12 );
	setBackground( Color.white );
    }
    
    public void start() // Appletens start(), *ej* trådens
    {
	animator = new Thread( this ); // Skapa en tråd
	animator.start();        
    }

    public void stop() // Appletens stop(), *ej* trådens
    {
	animator.stop();
        animator = null;
    }

    public void paint( Graphics g)
    {
	g.setColor(Color.red);
	g.fillOval(230-size*3/2, 97-size/2, size*3, size);
	if (shrinking)
	    { size -= 2; if (size == 0) shrinking = false;  }
	else
	    { size += 2; if (size == 100) shrinking = true; }
       	g.drawString( msg, 100, 100);		
    }

    
    public void run()
    {
	while (true)
	    {
		repaint();
		try { Thread.sleep(sleepingTime); }
		catch (InterruptedException e) { System.exit(1); }
	    }
    }
    
    public void destroy()
    {
	if ( animator != null ) 
	    {
		animator.stop();
		animator = null; // För att hjälpa skräphanteraren ...
	    }
    }
    
};
Om man söver ner en tråd bör man alltid fånga eventuella avbrottsundantag på det sätt som görs i koden.

Dubbelbuffring

Appleten i föregående avsnitt kanske (det beror på nätbläddrare, dator, andra appliktioner, etc ) flimrar på ett obehagligt sätt. Det finns flera tekniker att minska flimret och en vanlig teknik är s.k. dubbelbuffring, d.v.s man ritar sin bild i ett "fönster" i minnet och överför sedan hela bilden till skärmen. I Java är det lätt att ordna detta genom att ersätta metoden update():

Koden finns här

Ytterligare ett exempel på dubbelbuffring är det tidigare exemplet där koden ser ut så här med dubbelbuffring. I större applikationer kan man uppnå mycket genom att rita om bara den del av appleten som ändrats i stället för att rita om allt som vi gör i dessa exempel.

Synkronisering

Olika trådar arbetar ibland på samma data. För att detta skall fungera krävs att trådarna synkroniseras så att inte en av trådarna läser data som inte finns t.ex. Appleten här visar en producent-konsument situation med ett lager med plats för 20 varor av något slag.

Programmet har tre klasser: SynchronizerDemo, Producer och Consumer varav de båda senare exekverar i varsin tråd och alltså asynkront placerar varor i lagret respektive tar bort dem. För att göra det hela litet mer intressant finns en slumpmässig försening i klasserna. Dessutom kan man suspendera och starta om både producent och konsument. När lagret är fullt och producenten vill lägga in en vara (men alltå måste vänta) visas en röd markering på "producentsidan" och om lagret är tomt och konsumenten försöker hämta en vara visas motsvarande markering på "konsumentsidan". Lagret visas som en stapel i mitten.

Vi tittar först på valda delar av klassen SynchronizerDemo (de delar som har med den grafiska presentationen utelämnas):

public class SynchronizerDemo extends Applet
{
    final int bufferSize = 20;
    int inStore = 0;
    private Producer producer =  null;
    private Consumer consumer =  null;

 public void init() 
    {
        consumer = new Consumer( this );
        producer = new Producer( this );
    }

    synchronized void get()
    {
        if (inStore == 0)
            {
                setEmpty( true );
                try  { wait(); }
                catch ( InterruptedException e) {}
            }
        if ( (inStore--) == bufferSize )  // Om bufferten var full *innan*
            notify(); // vi tog ett element meddelar vi att nu finns det plats

    }
    
    synchronized void put()
    {
        if (inStore == bufferSize)
            {
                try { wait(); }
                catch ( InterruptedException e) {}
            }
        if ( (inStore++) == 0) // Om bufferten var tom *innan* vi lade dit ett
            notify();     // element meddelar vi att nu finns element att hämta
    }

    public void destroy()
    {
        if ( producer != null ) { producer.stop(); producer = null; }
        if ( consumer != null ) { consumer.stop(); consumer = null; }
    }
}
Man använder kvalificeraren synchronized för att synkronisera metoder: alla metoder som inte får exekvera samtidigt deklareras synchronized. Under tiden som en synkroniserad metod exekverar kan alltså ingen annan synkroniserad metod startas.

De andra verktygen för synkronisering är metoderna wait() och notify(). Man anropar wait() i en synkroniserad metod för att låta en annan tråd exekvera så att skälet till att man väntar kan upphävas. notify() är motsatsen till wait(): man väcker upp en väntande tråd efter att något skett med objektet, i vårt fall att lagret inte längre är tomt respektive att det inte längre är fullt. Det objekt som väcks är det som har egenrätt på SynchronizerDemo-instansen just nu, d.v.s. det objekt som startade en synkroniserad metod.

Observera att metoden wait() alltså släpper "låset" på objektet SynchronizerDemo så att producenten eller konsumenten kan uppdatera objektet.

Klasserna Producer och Consumer (även de något förenklade genom att grafiken tagits bort):

class Producer extends Thread
{
    SynchronizerDemo demo;
    long sleepingTime = 500;

    Producer( SynchronizerDemo d ) 
    {
        demo = d;
        start();
    }

    public void run()
    {
        while (true) 
            {
                try { if (Math.random()>0.9) sleep(sleepingTime); }
                catch ( InterruptedException e) {}
                demo.put();
            }
    }
}

class Consumer extends Thread
{
    SynchronizerDemo demo;
    long sleepingTime = 500;

    Consumer( SynchronizerDemo  d)
    {
        demo = d;
        start();
    }

    public void run()
    {
        while (true) 
            {
                try { if (Math.random()>0.9) sleep(sleepingTime); } 
                catch ( InterruptedException e) {}
                demo.get();
            }
    }
}

Konsumenten och producenten är båda arvtagare till Thread och har alltså redan ärvt gränssnittet Runnable genom Thread. Det kanske mest intressanta med klasserna är att ingendera innehåller några konstigheter: all synkronisering sker i klassen SynchronizerDemo. Allt Producer och Consumer gör är att producera och konsumera så fort de kan.

Den fullständiga programkoden visar också hur man kan implementera en ActionListener som ser efter vilken knapp som användaren tryckte på.

Multithreading

Multithreading är att flera trådar kan exekvera samtidigt i ett program. Vi har redan i exemplet SynchronizerDemo sett att två trådar kan exekvera samtidigt och till och med synkroniserat dem. Med multithreading avses oftast att flera trådar arbetar asynkront: t.ex. att en tråd utför något tungt i bakgrunden medan en annan tråd håller igång användargränssnittet.

Ett roligt (?) exempel (som också visar på skillnaden i effektivitet hos några sorteringsalgoritmer) på multithreading finns här

Strömmar

De flesta program måste kunna läsa och/eller skriva data. Det kan gälla information på en disk, någonstans på nätet, i minnet eller någon annanstans. Java har ett gemensamt gränssnitt för all sådan dataöverföring: strömmar (eng: streams). Algoritmer och metoder för att läsa eller skriva data ser exakt likadana ut oavsett varifrån man läser eller var man skriver.

Javas stream-klasser finns i java.io-paketet. Klasserna kan delas upp litet olika beroende på synsätt: en uppdelning kan vara huruvida man arbetar med tecken eller bytedata (bilder, ljud, etc): i Java är denna uppdelning tydlig:

En annan indelningsgrund är huruvida klasserna bara läser/skriver data eller om klassen också på något sätt bearbetar det som läses/skrives: se vidare här.

Grundklasserna

De fyra abstrakta superklasserna Reader, Writer, InputStream och OutputStream har liknande gränssnitt för enkel läsning/skrivning:
Reader: (läser tecken eller arrayer av tecken)
 int read(); // läser ett tecken och returnerar det i en int
 int read( char buf[] ); // läser högst buf.length  tecken, returnerar antalet

InputStream: (läser bytes eller arrayer av bytes)
 int read(); // läser ett tecken och returnerar det i en int
 int read( byte buf[]);// läser högst buf.length bytes, returnerar antalet

Writer: (skriver tecken eller arrayer av tecken)
 int write( int c); // skriver ett tecken (c)
 int write( char buf[] ); // skriver en array (buf.length st.) av tecken

OutputStream: (skriver bytes eller arrayer av bytes)
 int write( int c); // skriver en byte
 int write( byte buf[] ); // skriver en array (buf.length) av bytes
Dessutom finns för samtliga klasser en metod som läser eller skriver till en specificerad (med offset och längd) del av buf. Läsmetoderna väntar tills data finns tillgängligt eller end-of-file påträffas. Observera att man alltså inte kan vara säker på att hela arrayen läses (data behöver ju inte finnas än!).

Skrivning till skärmfönster och läsning från tangentbord

Strömmar används också vid enkel terminalbaserad läsning/skrivning. Instanser av de två klasserna PrintStream och InputStream används. I klassen System (en av de klasser som innehåller bara klassmetoder och därför inte kan instansieras) finns de tre strömmarna in, out och err. De två första används för normal läsning och skrivning medan err används för att rapportera fel. Alla tre är alltid öppna och redo att användas

Metoder för skrivning:

print( argument ); // skriv argumentet
println( argument ); // skriv argumentet, följt av en newline
där argumentet kan vara vad som helst som har en strängrepresentation (alla inbyggda typer, strängar och alla objekt som är instanser av klasser med en toString-metod).

Eftersom operatorn + fungerar för strängar kan man skriva ut flera saker i samma print under förutsättning att de objekt man skriver är instanser av klasser med en toString()-metod. I Java har man ingen kontroll över hur många siffror som skrivs ut vid utmatning av flyttal t.ex.

Vid läsning måste litet mer jobb till (InputStream innehåller endast de byte-baserade inläsningsmetoderna). Man gör då så att man skapar en InputStreamReader t.ex:

InputStreamReader = new InputStreamReader( System.in );
Denna klass fungerar som en "översättare" från byte-data till textdata. I praktiken brukar man också använda en BufferedReader för att öka effektiviteten.
BufferedReader = new BufferedReader( new InputStreamReader( System.in ));
Man måste gå "omvägen" över InputStreamReader eftersom BufferedReaders konstruktor ska ha en instans av Reader eller någon arvinge till denna. BufferedReader och InputStreamReader innehåller båda de metoder för läsning som beskrevs ovan plus:
String readLine()  - läs en rad
Man kan inte direkt läsa in instanser av de inbyggda typerna. För att läsa vanliga tal t.ex. kan man använda någon av klasserna StringTokenizer eller StreamTokenizer, som delar upp en sträng respektive det som läses från en ström i instanser av String och sedan använda de metoder som beskrevs ovan i avsnittet om strängar.

Java är inte speciellt bra på denna typ av I/O; förklaringen ligger i att moderna program använder grafiska användargränssnitt och därför inte behöver ha tillgång till ett fullständigt utbyggt bibliotek för textmässig läsning och skrivning. Det är ju faktiskt också så att program som inte har råd att krascha inte kan läsa in tal på annat sätt än genom egen parsning och för detta finns stöd i Java.

Textfiler

Textfiler hanteras analogt med ovanstående: InputStreamReader ersätts av FileReader (filnamnet anges i konstruktorn) och PrintStream av FileWriter (också med filnamn som konstruktorargument). För att få tillgäng till metoder för metoder för textformattering kan sedan klassen PrintWriter användas:
BufferedReader infile = new BufferedReader( new FileReader("filnamn.ny") );
PrintWriter outfile = new PrintWriter( new FileWriter("filnamn.old") );
Eftersom instanserna av FileReader och FileWriter inte används förutom i konstruktorn finns ingen anledning att namnge dem utan de skapas direkt vid konstruktionen av sina "wrapper"-klasser.

De båda instanserna infile och outfile kan nu användas exakt som klasserna i föregående avsnitt.

Ett exempel får avsluta detta avsnitt. Programmet nedan läser en textfil och räknar, dels totala antalet ord, dels också hur många gånger ett antal ord förekommer. Resultatet skrivs till skärmfönstret. Programmer använder en StreamTokenizer för att dela upp text i ord:

import java.io.*;

class Grep
{
    public static void main( String[] args ) throws IOException
    {
	StreamTokenizer infile = 
	    new StreamTokenizer( new FileReader( args[0] ) );
	int numWordsToLookFor = args.length;
	int[] numWords  = new int [ numWordsToLookFor ];
	for (int i=0; i < numWordsToLookFor; i++)
	    numWords[i] =  0;
	while ( infile.nextToken() != StreamTokenizer.TT_EOF )
	    if (infile.ttype == infile.TT_WORD )
		{
		    numWords[0]++; // räknar totala antalet ord
		    for (int i=1; i < numWordsToLookFor; i++)
			if ( infile.sval.equals( args[i] ))
			    numWords[i]++;
		}
	System.out.println( "Totala antalet ord: " + numWords[0] );
	for (int i=1; i<numWordsToLookFor; i++)
	    System.out.println( args[i] + ": " + numWords[i] );
    }
}

Klassen StreamTokenizer beskrivs närmare här. Programmet ovan öppnar den fil som ges som första argument till programmet, lägger en StreamTokenizer som ett lager runt filobjektet, samt räkner hur många gånger de ord som ges som extra argument förekommer i filen. Om man ger kommandot
java Grep Grep.java infile numWords
skriver programmet ut
Totala antalet ord: 69
infile: 1
numWords: 6

Binärfiler

För binärfiler (byte-data) finns som sagt en uppsättning klasser. Här visas det enkast tänkbara exemplet: filkopiering.
import java.io.*;

public class FileCopy
{
    public static final int BUFSIZE = 4096;

    public static void copy( String sourceName, String destinationName )
	throws IOException
    {
	FileInputStream source = null;
	FileOutputStream destination = null;
	try
	    {
		source =  new FileInputStream( sourceName );
		destination = new FileOutputStream( destinationName );
		byte[] buffer = new byte[BUFSIZE];
		int numBytesRead;
		while ( ( numBytesRead = source.read(buffer)) != -1)
		    destination.write( buffer, 0, numBytesRead);
	    }
	finally  // vi stänger alltid filerna
	    {
		if (source != null) 
		    { try { source.close(); } catch (IOException e) {} }
		if (destination != null)
		    { try { destination.close(); } catch (IOException e) {} }
		    
	    }
    }

    public static void main( String[] args ) throws IOException
    {
	copy ( args[0], args[1] );
    }
	
}
Liksom i tidigare exempel med strömmar saknas felhantering m.m. ovan. Java tillhandahåller en klass, File , som instansieras med ett filnamn. Instansen kan sedan användas för att ta reda på allehanda information om filen.

Mer om hur man använder strömmar, och koordination mellan asynkron input och output, kommer i avsnittet om nätverksprogrammering. I filkopieringsprogrammet ovan läses troligen 4096 bytes varje gång (utom den sista) men om filen ligger på en annan dator någonstans i världen kan man inte vara säker på att så är fallet.