Java: Föreläsning 4

Av Thomas Padron-McCarthy (Thomas.Padron-McCarthy@tech.oru.se). Senaste ändring 5 november 2003.

Ungefär motsvarande föreläsningsanteckningar från förra året: Tråd- och nätverksdelarna av java013.pdf

Innehåll i föreläsning 4

Trådar

Olika typer av parallell exekvering: Olika typer av parallell exekvering:

Moderkort för en fysisk processor
Moderkort för en fysisk processor
    Moderkort för två fysiska processorer
Moderkort för två fysiska processorer

Riktigt tråkigt exempel

Filen BoringThread.java:
public class BoringThread extends Thread {
    public void run() {
        while (true)
            ;
    } // run

    public static void main(String[] args) {
        BoringThread t1 = new BoringThread();
        t1.start();
        BoringThread t2 = new BoringThread();
        t2.start();
    } // main
} // class BoringThread
Metod: Tänk på att main-metoden i koden ovan egentligen inte har med en BoringThread-tråd att göra, trots att den är deklarerad i den klassen. Den måste bara stoppas in någonstans, och när vi nu har en klass är det onödigt att göra en extra klass bara för att main ska ligga där.

Aningen mindre tråkigt exempel

Filen BoringThread2.java:
public class BoringThread2 extends Thread {
    static int numberOfThreads = 0;
    int threadNumber = ++numberOfThreads;

    public void run() {
        int nrReps = 0;
        while (true) {
            System.out.println("Tråd " + threadNumber + " har kört " +
                               ++nrReps + " varv i loopen.");
        }
    } // run

    public static void main(String[] args) {
        BoringThread2 t1 = new BoringThread2();
        BoringThread2 t2 = new BoringThread2();
        t1.start();
        t2.start();
    } // main
} // class BoringThread2
Körexempel:
Tråd 1 har kört 1 varv i loopen.
Tråd 1 har kört 2 varv i loopen.
Tråd 1 har kört 3 varv i loopen.
  1296 rader från tråd 1 strukna
Tråd 1 har kört 1300 varv i loopen.
Tråd 1 har kört 1301 varv i loopen.
Tråd 1 har kört 1302 varv i loopen.
Tråd 2 har kört 1 varv i loopen.
Tråd 2 har kört 2 varv i loopen.
Tråd 2 har kört 3 varv i loopen.
Tråd 2 har kört 4 varv i loopen.
Tråd 2 har kört 5 varv i loopen.
Tråd 2 har kört 6 varv i loopen.
Tråd 2 har kört 7 varv i loopen.
Tråd 2 har kört 8 varv i loopen.
Tråd 2 har kört 9 varv i loopen.
Tråd 2 har kört 10 varv i loopen.
Tråd 1 har kört 1303 varv i loopen.
Tråd 1 har kört 1304 varv i loopen.
Tråd 1 har kört 1305 varv i loopen.
  4195 rader från tråd 1 strukna
Tråd 1 har kört 5501 varv i loopen.
Tråd 1 har kört 5502 varv i loopen.
Tråd 1 har kört 5503 varv i loopen.
Tråd 2 har kört 11 varv i loopen.
Tråd 2 har kört 12 varv i loopen.
Tråd 2 har kört 13 varv i loopen.
...
Lärdom: En tråd körs när Java-maskinen får lust. Skriv inte trådade program som bygger på att saker ska köras i någon viss ordning. (I alla fall inte utan att synkronisera trådarna, så de garanterat körs så.)

Exempel på tillämpningar

Ännu ett trådexempel

En trådklass Presenter (som i presenterare på engelska, inte julklappar) presenterar en textsträng. Presentation förskjuts åt höger. Tre presenterartrådar körs parallellt: en var 100:e ms, en var 200:e, en var 300:e. Filen Presenter.java:
public class Presenter extends Thread {
    private String message;
    private double sleepTime;
    private int steps;

    public Presenter(String message, double sleepTime) {
        this.message = message;
        this.sleepTime = sleepTime;
        this.steps = 0;
        start();
    }

    public void run() {
        while (true) {
            for (int i = 0; i < this.steps; ++i)
                System.out.print(" ");
            ++this.steps;
            System.out.println(this.message);
            try {
                Thread.sleep((int)(this.sleepTime * 1000));
            }
            catch (InterruptedException e) {
                System.out.println("Oj! Sömnen avbruten!");
            }
        }
    } // run

    public static void main(String[] args) {
        Presenter t1 = new Presenter("Turbo", 0.1);
        Presenter t2 = new Presenter("Svensson", 0.2);
        Presenter t3 = new Presenter("Skalman", 0.3);
    } // main
} // class Presenter
Körexempel:
Turbo
Svensson
Skalman
 Turbo
 Svensson
  Turbo
 Skalman
   Turbo
  Svensson
    Turbo
     Turbo
  Skalman
   Svensson
      Turbo
       Turbo
    Svensson
        Turbo
   Skalman
         Turbo
     Svensson
          Turbo
           Turbo
    Skalman
      Svensson
            Turbo
             Turbo
       Svensson
              Turbo
     Skalman
               Turbo
        Svensson
                Turbo
                 Turbo
      Skalman
         Svensson
                  Turbo
                   Turbo
          Svensson
                    Turbo
       Skalman
...

Ett problem med trådad programmering

Ibland kan det bli konstiga utskrifter:
...
                                         Turbo
                     Svensson
                                          Turbo
                                                                 Svensson
Turbo
               Skalman
                                            Turbo
                       Svensson
...
Det beror på att utskrifterna i två trådar kan råka komma samtidigt, och blandas med varandra.

Synkronisera. En lösning:

    public void run() {
        while (true) {
            synchronized ("fnord") {
                for (int i = 0; i < this.steps; ++i)
                    System.out.print(" ");
                ++this.steps;
                System.out.println(this.message);
            }
            try {
                Thread.sleep((int)(this.sleepTime * 1000));
            }
            catch (InterruptedException e) {
                System.out.println("Oj! Sömnen avbruten!");
            }
        }
    } // run

Det finns ett "synchronized-lås" per Java-objekt. En strängliteral i ett Java-program (som "fnord") är ett enda, unikt objekt.

Gränssnittet Runnable

Ovan ärvde vi från Thread. Men om vi inte kan det, för att vi måste ärva från något annat, till exempel Applet? (Java har ju inte multipelt arv.)

Lösning: Implementera gränssnittet Runnable, som bara säger att det måste finnas en run-metod.

Filen BoringRunnable.java:

public class BoringRunnable implements Runnable {
    public void run() {
        while (true)
            ;
    } // run

    public static void main(String[] args) {
        BoringRunnable r1 = new BoringRunnable();
        Thread t1 = new Thread(r1, "Tråd 1");
        t1.start();
        BoringRunnable r2 = new BoringRunnable();
        Thread t2 = new Thread(r2, "Tråd 2");
        t2.start();
    } // main
} // class BoringRunnable
Metod:

Trådar i Java

En tråds tillstånd

synchronized

Då kod inte får avbrytas, typiskt vid samtidig access av data. Exempel: En tråds arbete får inte avrbytas mitt i, för data kan vara i ett inkonsistent tillstånd! Filen BufferExample.java (utan felkontroll):
class Buffer {
    private String[] queue;
    private int number;

    public Buffer (int size) {
        queue = new String[size];
        number = 0;
    } // Buffer

    public synchronized void put(String str) {
        queue[number++] = str;
    } // put

    public synchronized String get() {
        String s = queue[0];
        for (int i = 1; i < number; ++i)
            queue[i - 1] = queue[i];
        --number;
        return s;
    } // get
} // class Buffer

class WriterThread extends Thread {
    Buffer b;
    public WriterThread(Buffer b) {
        this.b = b;
    }
    public void run() {
        int nrReps = 0;
        while (true) {
            String s = "Sträng nummer " + ++nrReps;
            System.out.println("WriterThread: Lagrar " + s);
            b.put(s);
        }
    } // run
} // class WriterThread

class ReaderThread extends Thread {
    Buffer b;
    public ReaderThread(Buffer b) {
        this.b = b;
    }
    public void run() {
        while (true) {
            String s = b.get();
            System.out.println("ReaderThread: Hämtade " + s);
        }
    } // run
} // class ReaderThread

public class BufferExample {
    public static void main(String[] args) {
        Buffer b = new Buffer(100);
        WriterThread w = new WriterThread(b);
        ReaderThread r = new ReaderThread(b);
        w.start();
        r.start();
    } // main
} // class BufferExample

wait och notify

Ovanstående exempel (BufferExample) kraschar efter en stund, antingen för att kön blir överfull eller "undertom". Både get och put måste kolla antalet element i kön, och kanske vänta på den andra tråden! Tänk på att wait släpper synchronized-låset!

Ur filen NotifyExample.java:

    public synchronized void put(String str) {
        while(number == queue.length) {
            try {
                System.out.println("Buffer.put: Väntar...");
                wait();
            }
            catch (InterruptedException e) {
                System.out.println("Buffer.put: Oj!");
            }
        }
        queue[number++] = str;
        notify();
    } // put

    public synchronized String get() {
        while(number == 0) {
            try {
                System.out.println("Buffer.get: Väntar...");
                wait();
            }
            catch (InterruptedException e) {
                System.out.println("Buffer.get: Oj!");
            }
        }
        String s = queue[0];
        for (int i = 1; i < number; ++i)
            queue[i - 1] = queue[i];
        --number;
        notify();
        return s;
    } // get

Java och nätverk

Java har bra stöd i språkets klassbibliotek för nätverkskommunikation. Jämför med C och C++, där språken i sig inte säger något om nätverk, utan man får använda operativsystemets funktioner och bibliotek. Det kan vara helt olika i olika operativsystem.

TCP/IP

Bakgrund. Inte på tentan.

En modell för nätverkskommunikation. Modellen har fyra nivåer, och på varje nivå kan det finnas flera olika protokoll. Ett protokoll är en samling regler för hur t. ex. kommunikation ska gå till.

De övre nivåerna bygger på de lägre. Om man till exempel hämtar en webbsida med sin webbläsare, är det förstås i grunden hårdvara, med dess drivrutiner, som används, dvs länklagret. Nätverkslagret använder länklagret för att skicka IP-paket. Transportlagret, här TCP, fyller IP-paketen med data, och skickar dem till rätt dator. Applikationslagret, här HTTP, bestämmer vilka data som ska skickas.

Ett IP-paket innehåller IP-adressen på den dator det ska till:

Ett IP-paket

Inuti IP-paketet kan man stoppa in ett data, med TCP-headrar. Headrarna innehåller till exempel numret på den port som datat ska till:

Ett TCP-paket

Portar

Bakgrund. Inte på tentan.

Enkel nätverkskommunikation: Hämta en webbsida

Filen ShowURL.java:
import java.net.URL;
import java.net.URLConnection;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

public class ShowURL {
    public static void main(String[] args) {
	try {
	    URL yahoo_url = new URL("http://www.yahoo.com/");
	    URLConnection yahoo_conn = yahoo_url.openConnection();
	    BufferedReader reader = new BufferedReader(
	        new InputStreamReader(
		    yahoo_conn.getInputStream()));
	    String buf;
	    while ((buf = reader.readLine()) != null) {
		System.out.println(buf);
		buf = reader.readLine();
	    } // while
	} // try
	catch (java.net.MalformedURLException e) {
	    System.out.println("Fel på webbadressen");
	}
	catch (java.io.IOException e) {
	    System.out.println("Fel när webbsidan skulle hämtas");
	}
    } // main
} // ShowURL

En applet som hämtar en webbsida

Före knapptryckning:

ShowURLApplet i Mozilla

Efter knapptryckning:

ShowURLApplet i Mozilla

Filen ShowURLApplet.java:

import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import javax.swing.*;

public class ShowURLApplet extends JApplet implements ActionListener {
    public void actionPerformed(ActionEvent event) {
	this.handle_button();
    } // actionPerformed

    private void handle_button() {
        try {
            URL yahoo_url = new URL("http://www.yahoo.com/");
	    getAppletContext().showDocument(yahoo_url);
        } // try
        catch (java.net.MalformedURLException e) {
            // Fel på webbadressen
        }
    } // handle_button

    public void init() {
	Container cp = getContentPane();
	cp.setLayout(new FlowLayout());

	JButton button = new JButton("Ladda YAHOO-sida");
	button.addActionListener(this);
	cp.add(button);
    } // init
} // ShowURLApplet
Appleten på riktigt (om det nu fungerar i din webbläsare):

Nätverkskommunikation med sockets

Filen Client.java:
import java.net.*;
import java.io.*;

public class Client {
    public static final int PORT = 2000;
    public static void main(String[] args) throws IOException {
        InetAddress addr;
        if (args.length >= 1)
            addr = InetAddress.getByName(args[0]);
        else
            addr = InetAddress.getByName(null);

        Socket socket = new Socket(addr, PORT);
        System.out.println("Den nya socketen: " + socket);

        BufferedReader in = new BufferedReader(
            new InputStreamReader(socket.getInputStream()));
        PrintWriter out = new PrintWriter(
            new BufferedWriter(
                new OutputStreamWriter(
                    socket.getOutputStream())), true);
        // true: PrintWriter is line buffered

        BufferedReader kbd_reader = new BufferedReader(
            new InputStreamReader(System.in));

        String buf;
        while (true) {
            buf = kbd_reader.readLine();
            System.out.println("Från tangentbordet: " + buf);
            System.out.println("Till servern: " + buf);
            out.println(buf);
            String inline = in.readLine();
            if (inline == null)
                break;
            System.out.println("Från servern: " + inline);
        }
    } // main
} // Client
Filen Server.java:
import java.io.*;
import java.net.*;

public class Server {
    public static final int PORT = 2000;
    public static void main(String[] args) throws IOException {
        ServerSocket s = new ServerSocket(PORT);
        System.out.println("Server-socketen: " + s);
        System.out.println("Servern lyssnar...");

        Socket socket = s.accept();
        System.out.println("Uppkoppling accepterad.");
        System.out.println("Den nya socketen: " + socket);

        BufferedReader in = new BufferedReader(
            new InputStreamReader(socket.getInputStream()));
        PrintWriter out = new PrintWriter(
            new BufferedWriter(
                new OutputStreamWriter(
                    socket.getOutputStream())), true);
        // true: PrintWriter is line buffered

        while (true) {
            String inline = in.readLine();
            System.out.println("Servern tog emot: " + inline);
            if (inline == null || inline.equals("quit")) // Not: inline == "quit"
                break;
            out.println("Du sa '" + inline + "'");
        }
    } // main
} // Server
Körexempel, klienten:
bimbatron > java Client localhost
Den nya socketen: Socket[addr=localhost/127.0.0.1,port=2000,localport=34029]
apa
Från tangentbordet: apa
Till servern: apa
Från servern: Du sa 'apa'
morot
Från tangentbordet: morot
Till servern: morot
Från servern: Du sa 'morot'
quit
Från tangentbordet: quit
Till servern: quit
bimbatron > 
Körexempel, servern:
bimbatron > java Server
Server-socketen: ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=2000]
Servern lyssnar...
Uppkoppling accepterad.
Den nya socketen: Socket[addr=/127.0.0.1,port=34029,localport=2000]
Servern tog emot: apa
Servern tog emot: morot
Servern tog emot: quit
bimbatron > 

Problem

Både servern och klienten är enkeltrådade: Lösningen är förstås att ha flera trådar: två trådar i klienten, och en tråd per klient i servern.

Nästa gång