Thread-Sicherheit in Java

Thread Safety in Java ist ein sehr wichtiges Thema. Java bietet Unterstützung für eine Multi-Thread-Umgebung mit Java Threads. Wir wissen, dass mehrere Threads, die von demselben Objekt erstellt wurden, Objektvariablen teilen und dies zu Dateninkonsistenz führen kann, wenn die Threads verwendet werden, um die gemeinsam genutzten Daten zu lesen und zu aktualisieren.

Thread-Sicherheit

Der Grund für die Dateninkonsistenz liegt darin, dass das Aktualisieren eines Feldwerts kein atomarer Prozess ist. Es erfordert drei Schritte: zunächst das Lesen des aktuellen Werts, dann die Durchführung der erforderlichen Operationen, um den aktualisierten Wert zu erhalten, und schließlich das Zuweisen des aktualisierten Werts an die Feldreferenz. Lassen Sie uns dies mit einem einfachen Programm überprüfen, bei dem mehrere Threads die gemeinsam genutzten Daten aktualisieren.

package com.journaldev.threads;

public class ThreadSafety {

    public static void main(String[] args) throws InterruptedException {
    
        ProcessingThread pt = new ProcessingThread();
        Thread t1 = new Thread(pt, "t1");
        t1.start();
        Thread t2 = new Thread(pt, "t2");
        t2.start();
        // Warten Sie, bis die Threads die Verarbeitung abgeschlossen haben
        t1.join();
        t2.join();
        System.out.println("Processing count="+pt.getCount());
    }

}

class ProcessingThread implements Runnable{
    private int count;
    
    @Override
    public void run() {
        for(int i=1; i < 5; i++){
            processSomething(i);
        	count++;
        }
    }

    public int getCount() {
        return this.count;
    }

    private void processSomething(int i) {
        // Verarbeiten einer Aufgabe
        try {
            Thread.sleep(i*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

In dem obigen Programm wird die Schleife count viermal um 1 erhöht, und da wir zwei Threads haben, sollte ihr Wert nach dem Ausführen beider Threads 8 sein. Wenn Sie das obige Programm jedoch mehrmals ausführen, werden Sie feststellen, dass der Wert von count zwischen 6, 7, 8 variiert. Dies geschieht, weil selbst wenn count++ wie eine atomare Operation erscheint, sie es nicht ist und Datenkorruption verursacht.

Thread-Sicherheit in Java

Thread-Sicherheit in Java ist der Prozess, unser Programm sicher für die Verwendung in einer Mehrfadenumgebung zu machen. Es gibt verschiedene Möglichkeiten, durch die wir unser Programm threadsicher machen können.

  • Die Synchronisation ist das einfachste und am weitesten verbreitete Mittel zur Gewährleistung der Thread-Sicherheit in Java.
  • Verwendung von Atomar Wrapper-Klassen aus dem Paket java.util.concurrent.atomic. Zum Beispiel AtomicInteger
  • Verwendung von Sperren aus dem Paket java.util.concurrent.locks.
  • Verwenden von threadsicheren Sammlungsklassen. Überprüfen Sie diesen Beitrag zur Verwendung von ConcurrentHashMap für Thread-Sicherheit.
  • Verwendung des Schlüsselwortes volatile mit Variablen, um sicherzustellen, dass jeder Thread die Daten aus dem Speicher liest und nicht aus dem Thread-Cache liest.

Java synchronized

Die Synchronisation ist das Mittel, mit dem wir Thread-Sicherheit erreichen können. Die JVM garantiert, dass synchronisierter Code nur von einem Thread gleichzeitig ausgeführt wird. Das Schlüsselwort synchronized in Java wird verwendet, um synchronisierten Code zu erstellen, und intern verwendet es Sperren auf Objekt oder Klasse, um sicherzustellen, dass nur ein Thread den synchronisierten Code ausführt.

  • Java-Synchronisation funktioniert durch Sperren und Entsperren der Ressource, bevor ein Thread den synchronisierten Code betritt. Er muss das Objekt sperren, und wenn die Code-Ausführung endet, wird die Ressource entsperrt, die von anderen Threads gesperrt werden kann. In der Zwischenzeit befinden sich andere Threads im Wartezustand, um die synchronisierte Ressource zu sperren.
  • Wir können das Schlüsselwort „synchronized“ auf zwei Arten verwenden: eine Methode komplett synchronisieren oder einen synchronisierten Block erstellen.
  • Wenn eine Methode synchronisiert ist, sperrt sie das Objekt. Wenn die Methode statisch ist, sperrt sie die Klasse. Daher ist es immer ratsam, einen synchronisierten Block zu verwenden, um nur die Abschnitte der Methode zu sperren, die Synchronisation benötigen.
  • Beim Erstellen eines synchronisierten Blocks müssen wir die Ressource angeben, auf die der Lock angewendet wird. Es kann XYZ.class oder ein Objektfeld der Klasse sein.
  • synchronized(this) sperrt das Objekt vor dem Betreten des synchronisierten Blocks.
  • Sie sollten das niedrigste Sperrniveau verwenden. Wenn es beispielsweise mehrere synchronisierte Blöcke in einer Klasse gibt und einer davon das Objekt sperrt, stehen andere synchronisierte Blöcke auch anderen Threads nicht zur Ausführung zur Verfügung. Wenn wir ein Objekt sperren, wird es auf alle Felder des Objekts gesperrt.
  • Java-Synchronisation gewährleistet die Datenintegrität auf Kosten der Leistung. Es sollte nur dann verwendet werden, wenn es unbedingt erforderlich ist.
  • Java-Synchronisation funktioniert nur in derselben JVM. Wenn Sie eine Ressource in einer Umgebung mit mehreren JVMs sperren müssen, funktioniert dies nicht, und Sie müssen sich um einen globalen Sperrmechanismus kümmern.
  • Java-Synchronisation könnte zu Deadlocks führen, überprüfen Sie diesen Beitrag über Deadlock in Java und wie man sie vermeidet.
  • Das Java-Synchronisations-Schlüsselwort kann nicht für Konstruktoren und Variablen verwendet werden.
  • Es ist vorzuziehen, ein Dummy-privates Objekt zu erstellen, das für den synchronisierten Block verwendet wird, damit dessen Referenz nicht von einem anderen Code geändert werden kann. Wenn Sie beispielsweise eine Setter-Methode für ein Objekt haben, auf das Sie synchronisieren, kann dessen Referenz durch einen anderen Code geändert werden, was zur parallelen Ausführung des synchronisierten Blocks führt.
  • Wir sollten kein Objekt verwenden, das im Konstantenpool verwaltet wird, zum Beispiel sollte String nicht zur Synchronisierung verwendet werden, weil wenn ein anderer Code auch auf dasselbe String sperrt, er versuchen wird, die Sperre auf dasselbe Referenzobjekt aus dem String-Pool zu erwerben, und obwohl beide Codes nicht zusammenhängen, werden sie sich gegenseitig sperren.

Hier sind die Änderungen am Code, die wir im obigen Programm vornehmen müssen, um es threadsicher zu machen.

    // Dummy-Objektvariable für Synchronisation
    private Object mutex=new Object();
    ...
    // Verwendung von synchronisierten Blöcken zum Lesen, Inkrementieren und Aktualisieren des Zählwerts synchron
    synchronized (mutex) {
            count++;
    }

Lassen Sie uns einige Synchronisationsbeispiele sehen und was wir daraus lernen können.

public class MyObject {
 
  // Sperren auf dem Monitor des Objekts
  public synchronized void doSomething() { 
    // ...
  }
}
 
// Hackercode
MyObject myObject = new MyObject();
synchronized (myObject) {
  while (true) {
    // Unbegrenztes Warten auf myObject
    Thread.sleep(Integer.MAX_VALUE); 
  }
}

Beachten Sie, dass der Hackercode versucht, die Instanz myObject zu sperren, und sobald er die Sperre erhält, sie nie freigibt, was dazu führt, dass die Methode doSomething() blockiert, während sie auf die Sperre wartet. Dies führt zu einem Deadlock im System und verursacht einen Denial-of-Service (DoS).

public class MyObject {
  public Object lock = new Object();
 
  public void doSomething() {
    synchronized (lock) {
      // ...
    }
  }
}

//nicht vertrauenswürdiger Code

MyObject myObject = new MyObject();
//Ändern des Lock-Objektverweises
myObject.lock = new Object();

Beachten Sie, dass das Lock-Objekt öffentlich ist und durch Ändern seines Verweises synchronisierte Blöcke parallel in mehreren Threads ausgeführt werden können. Ein ähnlicher Fall tritt auf, wenn Sie ein privates Objekt haben, aber eine Setter-Methode haben, um seinen Verweis zu ändern.

public class MyObject {
  //Sperren auf dem Monitor des Klassenobjekts
  public static synchronized void doSomething() { 
    // ...
  }
}
 
//Hackercode
synchronized (MyObject.class) {
  while (true) {
    Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
  }
}

Beachten Sie, dass der Hackercode einen Lock auf den Klassenmonitor bekommt und ihn nicht freigibt. Dies führt zu einem Deadlock und DoS im System. Hier ist ein weiteres Beispiel, bei dem mehrere Threads an demselben Array von Zeichenfolgen arbeiten und einmal verarbeitet, den Threadnamen an den Arraywert anhängen.

package com.journaldev.threads;

import java.util.Arrays;

public class SyncronizedMethod {

    public static void main(String[] args) throws InterruptedException {
        String[] arr = {"1","2","3","4","5","6"};
        HashMapProcessor hmp = new HashMapProcessor(arr);
        Thread t1=new Thread(hmp, "t1");
        Thread t2=new Thread(hmp, "t2");
        Thread t3=new Thread(hmp, "t3");
        long start = System.currentTimeMillis();
        //Alle Threads starten
        t1.start();t2.start();t3.start();
        //Warten auf das Beenden der Threads
        t1.join();t2.join();t3.join();
        System.out.println("Time taken= "+(System.currentTimeMillis()-start));
        //Überprüfen des Wertes der gemeinsam genutzten Variablen
        System.out.println(Arrays.asList(hmp.getMap()));
    }

}

class HashMapProcessor implements Runnable{
    
    private String[] strArr = null;
    
    public HashMapProcessor(String[] m){
        this.strArr=m;
    }
    
    public String[] getMap() {
        return strArr;
    }

    @Override
    public void run() {
        processArr(Thread.currentThread().getName());
    }

    private void processArr(String name) {
        for(int i=0; i < strArr.length; i++){
            //Daten verarbeiten und Threadnamen anhängen
            processSomething(i);
            addThreadName(i, name);
        }
    }
    
    private void addThreadName(int i, String name) {
        strArr[i] = strArr[i] +":"+name;
    }

    private void processSomething(int index) {
        //Bearbeiten einer Aufgabe
        try {
            Thread.sleep(index*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

Hier ist die Ausgabe, wenn ich das obige Programm ausführe.

Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]

Die Werte des String-Arrays sind aufgrund gemeinsamer Daten und fehlender Synchronisation beschädigt. Hier ist, wie wir die Methode addThreadName() ändern können, um unser Programm threadsicher zu machen.

    private Object lock = new Object();
    private void addThreadName(int i, String name) {
        synchronized(lock){
        strArr[i] = strArr[i] +":"+name;
        }
    }

Nach dieser Änderung funktioniert unser Programm einwandfrei, und hier ist die korrekte Ausgabe des Programms.

Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]

{
„error“: „Upstream error…“
}

Source:
https://www.digitalocean.com/community/tutorials/thread-safety-in-java