Thread Safety in Java

Thread Safety in Java is een zeer belangrijk onderwerp. Java biedt ondersteuning voor een multithreaded omgeving met behulp van Java Threads. We weten dat meerdere threads die zijn gemaakt vanuit hetzelfde object, objectvariabelen delen, en dit kan leiden tot inconsistentie van gegevens wanneer de threads worden gebruikt om gedeelde gegevens te lezen en bij te werken.

Thread Safety

De reden voor de inconsistentie van gegevens is omdat het bijwerken van de waarde van een veld geen atomair proces is. Het vereist drie stappen: ten eerste de huidige waarde lezen, ten tweede de noodzakelijke bewerkingen uitvoeren om de bijgewerkte waarde te verkrijgen, en ten derde de bijgewerkte waarde toewijzen aan de veldreferentie. Laten we dit controleren met een eenvoudig programma waarin meerdere threads de gedeelde gegevens bijwerken.

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();
        // wacht tot threads klaar zijn met verwerken
        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) {
        // verwerking van enkele taken
        try {
            Thread.sleep(i*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

In het bovenstaande programma wordt de for-lus gebruikt, waarbij count vier keer met 1 wordt verhoogd. Aangezien we twee threads hebben, zou de waarde na het voltooien van beide threads 8 moeten zijn. Maar wanneer je het bovenstaande programma meerdere keren uitvoert, zul je merken dat de waarde van count varieert tussen 6, 7, 8. Dit gebeurt omdat zelfs als count++ lijkt op een atomaire bewerking, is het dat NIET en veroorzaakt het gegevenscorruptie.

Thread Safety in Java

Thread safety in Java is het proces om ons programma veilig te maken voor gebruik in een multithreaded omgeving, er zijn verschillende manieren waarop we ons programma thread-safe kunnen maken.

  • Synchronisatie is het makkelijkste en meest gebruikte middel voor thread safety in Java.
  • Gebruik van Atomic Wrapper-klassen uit het java.util.concurrent.atomic-pakket. Bijvoorbeeld AtomicInteger
  • Gebruik van locks uit het java.util.concurrent.locks-pakket.
  • Gebruik van thread-safe collectieklassen, bekijk deze post voor het gebruik van ConcurrentHashMap voor thread safety.
  • Het gebruik van het volatiel trefwoord bij variabelen om ervoor te zorgen dat elke thread de gegevens uit het geheugen leest, en niet uit de thread-cache.

Java synchronized

Synchronisatie is het middel waarmee we thread-safe kunnen bereiken, de JVM garandeert dat gesynchroniseerde code slechts door één thread tegelijk wordt uitgevoerd. Het java-sleutelwoord synchronized wordt gebruikt om gesynchroniseerde code te maken en intern gebruikt het vergrendelingen op Object of Klasse om ervoor te zorgen dat slechts één thread de gesynchroniseerde code uitvoert.

  • Java-synchronisatie werkt door het vergrendelen en ontgrendelen van de resource voordat een thread de gesynchroniseerde code binnengaat. Het moet het slot op het object verkrijgen, en wanneer de code-uitvoering eindigt, ontgrendelt het de resource zodat andere threads deze kunnen vergrendelen. Ondertussen wachten andere threads in de wachtstand om de gesynchroniseerde resource te vergrendelen.
  • We kunnen het trefwoord ‘synchronized’ op twee manieren gebruiken. De ene is om een volledige methode te synchroniseren, en de andere manier is om een gesynchroniseerd blok te maken.
  • Wanneer een methode is gesynchroniseerd, vergrendelt het het Object. Als de methode statisch is, vergrendelt het de Class. Daarom is het altijd het beste om een gesynchroniseerd blok te gebruiken om alleen de secties van de methode te vergrendelen die synchronisatie nodig hebben.
  • Bij het maken van een gesynchroniseerd blok moeten we de resource opgeven waarop het slot zal worden verkregen. Dit kan XYZ.class zijn of een Object-veld van de klasse.
  • synchronized(this) vergrendelt het object voordat het het gesynchroniseerde blok binnengaat.
  • Je moet het laagste niveau van vergrendeling gebruiken. Bijvoorbeeld, als er meerdere gesynchroniseerde blokken in een klasse zijn en een daarvan vergrendelt het Object, zullen andere gesynchroniseerde blokken ook niet beschikbaar zijn voor uitvoering door andere threads. Wanneer we een Object vergrendelen, wordt een slot verkregen op alle velden van het Object.
  • Java-synchronisatie zorgt voor gegevensintegriteit ten koste van prestaties, dus het moet alleen worden gebruikt wanneer het absoluut noodzakelijk is.
  • Java-synchronisatie werkt alleen in dezelfde JVM. Als je een resource in meerdere JVM-omgevingen wilt vergrendelen, werkt dit niet en moet je mogelijk zorgen voor een globaal vergrendelingsmechanisme.
  • Java Synchronisatie kan leiden tot deadlocks, bekijk deze post over deadlock in java en hoe ze te vermijden.
  • Het Java synchronized trefwoord kan niet worden gebruikt voor constructors en variabelen.
  • Het is raadzaam om een dummy private Object te maken om te gebruiken voor het gesynchroniseerde blok, zodat de referentie ervan niet door andere code kan worden gewijzigd. Bijvoorbeeld, als je een setter methode hebt voor Object waarop je synchroniseert, kan de referentie ervan worden gewijzigd door andere code wat leidt tot parallelle uitvoering van het gesynchroniseerde blok.
  • We moeten geen object gebruiken dat wordt onderhouden in een constante pool, bijvoorbeeld String mag niet worden gebruikt voor synchronisatie omdat als andere code ook op dezelfde String vergrendelt, het probeert vergrendeling te verkrijgen op hetzelfde referentieobject uit de String pool en ook al zijn beide codes niet gerelateerd, ze zullen elkaar vergrendelen.

Hier zijn de code wijzigingen die we moeten doen in het bovenstaande programma om het thread-safe te maken.

    //dummy objectvariabele voor synchronisatie
    private Object mutex=new Object();
    ...
    //gebruik van gesynchroniseerd blok om count-waarde synchroon te lezen, te verhogen en bij te werken
    synchronized (mutex) {
            count++;
    }

Laten we eens kijken naar enkele synchronisatievoorbeelden en wat we ervan kunnen leren.

public class MyObject {
 
  // Vergrendelingen op het monitorobject
  public synchronized void doSomething() { 
    // ...
  }
}
 
// Hackerscode
MyObject myObject = new MyObject();
synchronized (myObject) {
  while (true) {
    // Onbepaalde vertraging myObject
    Thread.sleep(Integer.MAX_VALUE); 
  }
}

Let op dat de code van de hacker probeert om de instantie van myObject te vergrendelen en zodra het de vergrendeling krijgt, deze nooit vrijgeeft, waardoor de methode doSomething() wordt geblokkeerd tijdens het wachten op de vergrendeling, dit zal het systeem in een impasse brengen en een Denial of Service (DoS) veroorzaken.

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

//onbetrouwbare code

MyObject myObject = new MyObject();
//verander de referentie naar het vergrendelingsobject
myObject.lock = new Object();

Let op dat het vergrendelingsobject openbaar is en door de referentie te veranderen, kunnen we een gesynchroniseerd blok parallel uitvoeren in meerdere threads. Een soortgelijk geval is waar als je een privéobject hebt maar een settermethode hebt om de referentie te wijzigen.

public class MyObject {
  //vergrendelingen op de monitor van het klasse-object
  public static synchronized void doSomething() { 
    // ...
  }
}
 
//code van hackers
synchronized (MyObject.class) {
  while (true) {
    Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
  }
}

Let op dat de code van de hacker een vergrendeling krijgt op de klassemonitor en deze niet vrijgeeft, dit zal een impasse en DoS veroorzaken in het systeem. Hier is een ander voorbeeld waarbij meerdere threads werken aan dezelfde array van Strings en zodra verwerkt, de naam van de thread aan de arraywaarde toevoegen.

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();
        //start alle threads
        t1.start();t2.start();t3.start();
        //wacht tot threads klaar zijn
        t1.join();t2.join();t3.join();
        System.out.println("Time taken= "+(System.currentTimeMillis()-start));
        //controleer nu de waarde van de gedeelde variabele
        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++){
            //gegevens verwerken en threadnaam toevoegen
            processSomething(i);
            addThreadName(i, name);
        }
    }
    
    private void addThreadName(int i, String name) {
        strArr[i] = strArr[i] +":"+name;
    }

    private void processSomething(int index) {
        // bezig met een taak
        try {
            Thread.sleep(index*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

Hier is de uitvoer wanneer ik het bovenstaande programma uitvoer.

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

De String-arraywaarden zijn beschadigd vanwege gedeelde gegevens en gebrek aan synchronisatie. Hier is hoe we de methode addThreadName() kunnen wijzigen om ons programma thread-safe te maken.

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

Na deze wijziging werkt ons programma prima en hier is de juiste uitvoer van het programma.

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]

Dat is alles over threadveiligheid in Java, ik hoop dat je iets hebt geleerd over het programmeren van threadveiligheid en het gebruik van het synchronisatiesleutelwoord.

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