Threadsicherheit in Java: Beste Praktiken

Threadsicherheit in Java ist ein sehr wichtiges Thema. Java bietet Unterstützung für eine multithreaded Umgebung mithilfe von Java Threads. Wir wissen, dass mehrere Threads, die vom selben Objekt erstellt wurden, Objektvariablen teilen und dies kann zu Dateninkonsistenzen führen, wenn die Threads zum Lesen und Aktualisieren der gemeinsam genutzten Daten verwendet werden.

Verständnis von Threadsicherheit

Der Grund für Dateninkonsistenzen liegt darin, dass das Aktualisieren eines Feldwertes kein atomarer Prozess ist. Es erfordert drei Schritte: erstens das Lesen des aktuellen Wertes, zweitens das Durchführen der notwendigen Operationen, um den aktualisierten Wert zu erhalten, und drittens das Zuweisen des aktualisierten Wertes an die Feldreferenz. Lassen Sie uns dies mit einem einfachen Programm überprüfen, in dem mehrere Threads die gemeinsam genutzten Daten aktualisieren.

Beispielprogramm

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();
        // Warte, bis Threads die Verarbeitung abgeschlossen haben
        t1.join();
        t2.join();
        System.out.println("Verarbeitungsanzahl="+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) {
        // Verarbeitung einer Aufgabe
        try {
            Thread.sleep(i*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

In der obigen Programmschleife wird der Wert von count viermal um 1 erhöht, und da wir zwei Threads haben, sollte sein Wert 8 sein, nachdem beide Threads die Ausführung beendet haben. Wenn Sie jedoch das obige Programm mehrmals ausführen, werden Sie feststellen, dass der Wert von count zwischen 6, 7, 8 variiert. Dies geschieht, weil count++ zwar wie eine atomare Operation aussieht, es aber NICHT ist und zu Datenkorruption führt.

Threadsicherheit in Java gewährleisten

Threadsicherheit in Java ist der Prozess, um unser Programm sicher in einer multithreaded Umgebung zu verwenden. Es gibt verschiedene Möglichkeiten, wie wir unser Programm threadsicher machen können.

  • Synchronisierung ist das einfachste und am häufigsten verwendete Werkzeug für Threadsicherheit in Java.
  • Verwendung von Atomic Wrapper-Klassen aus dem Paket java.util.concurrent.atomic. Zum Beispiel AtomicInteger
  • Verwendung von Locks aus dem Paket java.util.concurrent.locks.
  • Verwendung von threadsicheren Sammlungsklassen, überprüfen Sie diesen Beitrag zur Verwendung von ConcurrentHashMap für Threadsicherheit.
  • Verwendung des Schlüsselworts volatile mit Variablen, um jeden Thread dazu zu bringen, die Daten aus dem Speicher zu lesen und nicht aus dem Thread-Cache.

Java Synchronized

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

Java-Synchronisierung arbeitet mit dem Sperren und Entsperren der Ressource, bevor ein Thread in den synchronisierten Code eintritt. Er muss das Lock auf dem Objekt erwerben, und wenn die Codeausführung endet, entsperrt er die Ressource, 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 Möglichkeit besteht darin, eine komplette Methode zu synchronisieren, und eine andere Möglichkeit besteht darin, einen synchronisierten Block zu erstellen.

Wenn eine Methode synchronisiert wird, sperrt sie das Objekt. Wenn die Methode statisch ist, sperrt sie die Klasse. Daher ist es immer die beste Praxis, einen synchronisierten Block zu verwenden, um nur die Abschnitte der Methode zu sperren, die Synchronisierung benötigen.

Beim Erstellen eines synchronisierten Blocks müssen wir die Ressource angeben, auf der das Lock erworben wird. Es kann XYZ.class oder ein beliebiges Objektfeld der Klasse sein.

synchronized(this) sperrt das Objekt, bevor es in den synchronisierten Block eintritt.

Sie sollten die niedrigste Ebene der Sperre verwenden. Wenn es zum Beispiel mehrere synchronisierte Blöcke in einer Klasse gibt und einer davon das Objekt sperrt, dann werden auch andere synchronisierte Blöcke für die Ausführung durch andere Threads nicht verfügbar sein. Wenn wir ein Objekt sperren, wird ein Lock auf alle Felder des Objekts erworben.

Java Synchronisierung bietet Datenintegrität auf Kosten der Leistung, daher sollte sie nur verwendet werden, wenn es absolut notwendig ist.

Java Synchronisierung funktioniert nur in derselben JVM, also wenn Sie eine Ressource in einer Mehrfach-JVM-Umgebung sperren müssen, wird es nicht funktionieren, und Sie müssen möglicherweise nach einem globalen Sperrmechanismus suchen.

Java Synchronisierung kann zu Deadlocks führen, überprüfen Sie diesen Beitrag über Deadlocks in Java und wie man sie vermeidet.

Das Schlüsselwort synchronized in Java kann nicht für Konstruktoren und Variablen verwendet werden.

Es ist vorzuziehen, ein Dummy privates Objekt zum Synchronisieren zu erstellen, so dass seine Referenz nicht durch anderen Code geändert werden kann. Wenn Sie beispielsweise eine Setter-Methode für das 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 in einem Konstantenpool gepflegt wird. Zum Beispiel sollte String nicht für Synchronisierung verwendet werden, da, wenn ein anderer Code auch auf denselben String sperrt, er versuchen wird, ein Lock auf dasselbe Referenzobjekt aus dem String-Pool zu erwerben, und obwohl beide Codes unabhängig voneinander sind, werden sie sich gegenseitig sperren.

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

    // Dummy-Objektvariable für die Synchronisation
    private Object mutex = new Object();
    ...
    // Verwendung eines synchronisierten Blocks zum synchronen Lesen, Inkrementieren und Aktualisieren des Zählwertes
    synchronized (mutex) {
        count++;
    }

Verständnis der Synchronisation in Java

Lassen Sie uns einige Beispiele für Synchronisation betrachten und was wir daraus lernen können.

Beispielszenarien

public class MyObject {

  // Sperrt auf dem Monitor des Objekts
  public synchronized void doSomething() { 
    // ...
  }
}

// Hacker-Code
MyObject myObject = new MyObject();
synchronized (myObject) {
  while (true) {
    // Unendliche Verzögerung von myObject
    Thread.sleep(Integer.MAX_VALUE); 
  }
}

In diesem Szenario versucht der Hacker-Code, die Instanz myObject zu sperren, und sobald er das Lock erhält, gibt er es nie wieder frei, was dazu führt, dass die doSomething()-Methode blockiert, weil sie auf das Lock wartet. Dies kann zu einem Deadlock und einer Denial-of-Service-Attacke (DoS) führen.

public class MyObject {
  public Object lock = new Object();

  public void doSomething() {
    synchronized (lock) {
      // ...
    }
  }
}

// Nicht vertrauenswürdiger Code

MyObject myObject = new MyObject();
// Ändert die Lock-Objektreferenz
myObject.lock = new Object();

Hier ist das Lock-Objekt öffentlich, und indem man seine Referenz ändert, können wir den synchronisierten Block parallel in mehreren Threads ausführen. Dies ist ein häufiges Problem, wenn das Lock-Objekt veränderbar oder öffentlich zugänglich ist.

public class MyObject {
  // Sperrt auf dem Monitor des Klassenobjekts
  public static synchronized void doSomething() { 
    // ...
  }
}

// Hacker-Code
synchronized (MyObject.class) {
  while (true) {
    Thread.sleep(Integer.MAX_VALUE); // Unendliche Verzögerung von MyObject
  }
}

In diesem Beispiel erhält der Hacker-Code ein Lock auf dem Klassenmonitor und gibt es nicht frei, was zu Deadlock und DoS im System führt.

Arbeiten mit Arrays und Synchronisation

Hier ist ein weiteres Beispiel, in dem mehrere Threads am selben Array von Strings arbeiten und 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();
// Starten aller Threads
t1.start();t2.start();t3.start();
// Warten, bis Threads fertig sind
t1.join();t2.join();t3.join();
System.out.println(„Zeit genommen= „+(System.currentTimeMillis()-start));
// Überprüfen Sie jetzt den Wert 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 Thread-Namen anhängen
processSomething(i);
addThreadName(i, name);
}
}

private void addThreadName(int i, String name) {
strArr[i] = strArr[i] +“:“+name;
}

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

}

Hier ist die Ausgabe, als ich das obige Programm ausgeführt habe.

Zeit genommen= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]

Die Werte des String-Arrays sind korrupt, weil die Daten gemeinsam genutzt werden und keine Synchronisierung stattfindet. 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.


Zeit genommen= 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]


Nachdem die addThreadName-Methode synchronisiert wurde, funktioniert das Programm korrekt ohne Datenkorruption.

Fazit

Das ist alles zur Threadsicherheit Praktiken in Java. Es ist entscheidend, threadsichere Programmierung und die Verwendung des Schlüsselworts synchronized zu verstehen, um Dateninkonsistenzen und potenzielle Sicherheitsprobleme in einer multithreaded Umgebung zu vermeiden.

Kostenlosen Account erstellen

Registrieren Sie sich jetzt und erhalten Sie Zugang zu unseren Cloud Produkten.

Das könnte Sie auch interessieren: