74. Synchronisation

Problem:
  • Threads müssen dazu gezwungen werden stets zu refreshen (vor einer Änderung) und zu flushen (nach einer Änderung) -> sequential consistency

  • Switchen des Schedulers an kritischen Stellen im Code muss verhindert werden (race conditions)

Lösung:

Synchronisierung der Threads

CrazyThread2.java -> SynchedThread2.java

SynchedThreads2.java
package programme;

public class SynchedThreads2 implements Runnable
{
	//static Object lock = new Object();

	int x;

	@Override
	public void run() {
		int i;
		for ( i = 0; i < 2000; i++){
			synchronized(this){
				x++;
			}
		}
		System.out.println(Thread.currentThread().getName() + " has finished after " + i + " loops");
	}

	public static void main(String[] args){

		SynchedThreads2 c = new SynchedThreads2();
		new Thread(c).start(); //Thread wird ohne Zuweisung an eine Variable gestartet
		new Thread(c).start(); //Thread wird abermals gestartet

		//Achtung: Beide Threads durchlaufen zeitgleich die run() Methode, d.h. erh�hen die Instanzvariable x
		//Die lokale Variable i teilen sich die beiden Threads nicht, sondern jeder besitzt ein eigenes lokales i

		try
		{
			Thread.sleep(5000);
		}
		catch (InterruptedException e)
		{
			e.printStackTrace();
		}

		synchronized(c){
			System.out.println("x is: " + c.x);
		}

		/*Ausgabe ist bei jeder Ausf�hrung unterschiedlich:
		Thread-1 has finished after 2000 loops
		Thread-0 has finished after 2000 loops
		x is: 3291
		- x ist h�ufig nicht die Summe aus Thread 1 und 2 (sollte stets 4000 sein)
		*/
	}

}
static Object lock = new Object();
    for ( i = 0; i < 2000; i++){
        synchronized(lock ){
            x++;
        }
    }

Änderungen:

  1. Es wird ein referenziertes Objekt initialisiert

  2. Das Inkrement x++ wird in einem synchronized-Codeblock ausgeführt: synchronized(<Verweis auf Object>){ Codeblock }

-> Der Verweis darf nicht den Wert null enthalten, daher darf kein nicht-referenzierte leeres Objekt verwendet werden

Folgende Schritte werden beim synchronized-Block abgearbeitet:

  1. Lock des referenzierten Object wird erlangt

    • jedes Objekt besitzt ein Lock (auch Monitor genannt)

    • sofern ein anderer Thread bereits den Lock hat wird gewartet bis er wieder frei ist

  2. Refresh vom globalen auf den lokalen Speicher wird ausgeführt

  3. Synchronisationsrumpf wird abgearbeitet

  4. Flush vom lokalen Speicher auf den globalen Speicher

  5. Lock wird wieder freigegeben

  • sequential consistency ist gewährleistet da sicheres refreshen und flushen vor und nach der Veränderung

  • race conditions Fehler passieren nicht, da ein Thread so lange pausieren muss, solange der das Lock nicht hat (nur ein Thread kann zeitgleich seinen Synchronsationsrumpf ausführen)

Wichtig:

  • Alle betroffenen Threads müssen auf das gleiche Objekt synchronisieren

  • Welches Objekt verwendet wird, ist nicht wichtig

  • Der main-Thread muss ebenfalls synchronisiert werden

z.B. SynchedThreads2.java

main-Thread -> verwendet Object c: synchronized (c)

Thread 1 -> verwendet ebenfalls Object c, also muss auf this gelockt werden: synchronized (this)

SynchedThread3.java:

SynchedThreads3.java
package programme;

public class SynchedThreads3 implements Runnable
{

	int x;

	@Override
	public void run()
	{
		int loops = 0;

		//x wird erh�ht, bis 10000 erreicht ist (egal welcher Thread x wie oft erh�ht)
		while (true)
		{
			synchronized (this)
			{
				if (x < 10000)
				{
					x++;
					loops++;
				}
				else
				{
					break;
				}
			}
		}
		System.out.println(Thread.currentThread().getName() + " has finished after " + loops + " loops");
	}

	public static void main(String[] args)
	{
		SynchedThreads3 a = new SynchedThreads3();
		new Thread(a).start();
		new Thread(a).start();

		try
		{
			Thread.sleep(500);
		}
		catch (InterruptedException e)
		{
			e.printStackTrace();
		}
		synchronized (a)
		{
			System.out.println("x is: " + a.x);
		}

		/*
		 * Ausgabe ist bei jeder Ausf�hrung unterschiedlich, da man nicht
		 * beeinflussen kann, welcher Thread wann startet (Scheduling): Thread-1
		 * has finished after 6305 loops Thread-0 has finished after 8700 loops
		 * x is: 10001 - x(thread1) + x(thread2) ist stets �ber 10000 (sollte in
		 * der Summe genau 10000 sein) - x ist manchmal auch mehr als 10000
		 * (sollte auch stets 10000 sein)
		 */
	}

}
while (true )
    {
        synchronized (this )
        {
            if (x < 10000)
            {
                x++;
                loops++;
            }
            else
            {
                break;
            }
        }
    }

Hinweis:

  • Der Synchronized Codeblock darf nicht innerhalb der if-Bedingung gesetzt werden, da dann noch immer nach der Prüfung x < 10000 der Thread gewechselt werden kann und somit das Ergebnis 10001 möglich ist

  • Wäre die while-Schleife nicht in if-else getrennt, würde der erste Thread die Inkrementierung vollständig abschließen (erst bei 10000 wird der Block verlasssen). Der zweite käme nicht zum Zug:

    synchronized(this ){
        while(x <10000) {
            x++;
            loops++;
        }
    }
    

    Ergebnis:

    • Thread-1 has finished after 0 loops

    • Thread-0 has finished after 10000 loops

    • x is: 10000

SynchedThread1.java:

SynchedThreads1.java
package programme;

public class SynchedThreads1 implements Runnable
{

	volatile boolean alive;
	volatile long i;

	@Override
	public void run()
	{
		System.out.println("Thread starts...");
		i = 0;
		alive = true;

		while (true) //Langfassung f�r while (alive = true) { i++; }
		{
			synchronized (this)
			{ //muss hier rein, damit main-Thread alive wieder auf false setzen zu k�nnen
				if (alive)
				{
					i++;
				}
				else
				{
					break;
				}
			}
		}
		System.out.println("Thread ends...");
	}

	public static void main(String[] args)
	{
		SynchedThreads1 c = new SynchedThreads1(); //ein zweiter Thread wird erzeugt (zus�tzlich zu main())
		new Thread(c).start(); //run() Methode der Instanz-Methode wird in einem neuen Thread ausgef�hrt

		// Die Methode pr�ft den aktuellen Wert von i im zweiten Thread. �bersteigt dies 1 Milliarde, so wird der Thread beendet
		while (Thread.activeCount() > 1) //mindestens zwei gerade aktive Threads
		{

			try
			{
				Thread.sleep(500);
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}

			long iNow;
			synchronized(c){
				iNow = c.i;
			}

			System.out.println(iNow);
			if (iNow >= LIMIT)
			{
				synchronized(c){
				c.alive = false;
				}
				System.out.println("THREAD STOPPED! (alive = " + c.alive + ")");
			}

		}
		System.out.println("Program exits...");
		/*
		 * Das Programm endet nie (die if-Schleife wird nie verlassen) Auch wenn
		 * alive = false ist und i bereits 1 Milliarde �berschritten hat l�uft
		 * das Programm ewig weiter
		 */
	}

	private static final long LIMIT = 300000000L;

}
  • Ebenfalls den Rumpf der while-Schleife synchronisieren

  • Im main-Thread muss nicht der ganze Rumpf, sondern nur die einzelnen Zugriffe synchronisiert werden

Hinweise:

  • Flushen und Refreshen kostet Zeit

  • Optimierung von Synchronisationsmomenten ist eine eigene Disziplin

  • Je mehr synchronisiert wird, desto geringer wird der Geschwindigkeitsvorteil von Multi-Thread

  • Wiederholung: die stop() Methode sollte nicht zum Beenden eines Threads verwendet werden, weil dieser beendete Thread sein lock dadurch frei gibt. Die Synchronized-Blocks der anderen Threads die das gleiche Lock verwenden werden dadurch frei zum Start. Falls eine Veränderung zu diesem Zeitpunkt nicht wie gewünscht abgeschlossen war, führt dies womöglich zu beliebigem Verhalten des Objekts, sofern andere Threads eingreifen

MultiThreadingDemo.java:

MultiThreadingDemo.java
package programme;

import klassen.MyTask;

public class MultiThreadingDemo
{

	public static void main(String[] args)
	{
//		int threads = 4;
//
//		for(int i = 1; i <= threads; i++){
//			MyTask t = new MyTask();
//			Thread thread = new Thread(t);
//			thread.start();
//		}

		MyTask t = new MyTask();
		new Thread(t).start();

		System.out.println("MAIN-THREAD L�UFT...");
		try
		{
			Thread.sleep(3000);
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}

		synchronized(t){
			t.cancel();
		}


	}

}
  • Das Programm funktioniert scheinbar auch ohne Synchronisation (vermutlich führen bestimmte Anweisungen in der MyTask.java zum Refresh und Flushen)

  • man sollte sich jedoch auf so etwas nie verlassen und stets Synchronisationsblöcke verwenden

Die alive-Abfrage in der MyTask Methode:

synchronized (this )
    {
        if (!alive )
        {
             System. out.println("Stopping!" );
             return;
        }
    }

und den Aufruf der cancel()-Methode in der main-Methode:

synchronized(t){
    t.cancel();
}

oder in der alive-Abfrage der cancel() Methode selbst:

public void cancel()
    {
        synchronized(this ){
            alive = false;
        }
    }

Tip: Synchronisieren einer kompletten Methode (z.B. cancel())

public synchronized void cancel()
{
    alive = false;
}

Es ist auch möglich statische Methoden zu synchronisieren:

public static synchronized void x() {
    synchronized(MyTask.class ){

    }
}

Hier wird direkt auf die Objekt MyTask.class synchronisiert (wie das genau abläuft ist hier egal).