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
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:
Es wird ein referenziertes Objekt initialisiert
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:
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
Refresh vom globalen auf den lokalen Speicher wird ausgeführt
Synchronisationsrumpf wird abgearbeitet
Flush vom lokalen Speicher auf den globalen Speicher
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:
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:
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:
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).