La sicurezza del thread in Java è un argomento molto importante. Java fornisce il supporto per l’ambiente multithread utilizzando i Thread di Java, sappiamo che i thread multipli creati dallo stesso oggetto condividono le variabili dell’oggetto e questo può portare a incoerenza dei dati quando i thread vengono utilizzati per leggere e aggiornare i dati condivisi.
Sicurezza del thread
La ragione dell’incoerenza dei dati è perché l’aggiornamento di qualsiasi valore di campo non è un processo atomico, richiede tre passaggi: prima leggere il valore attuale, secondo eseguire le operazioni necessarie per ottenere il valore aggiornato e terzo assegnare il valore aggiornato al riferimento del campo. Verifichiamolo con un semplice programma in cui più thread aggiornano i dati condivisi.
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();
//attendere il completamento dell'elaborazione dei thread
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) {
//elaborare qualche lavoro
try {
Thread.sleep(i*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Nel programma sopra, nel ciclo for, count viene incrementato di 1 quattro volte e poiché abbiamo due thread, il suo valore dovrebbe essere 8 dopo che entrambi i thread hanno finito di eseguire. Ma quando eseguirai il programma sopra più volte, noterai che il valore di count varia tra 6,7,8. Questo succede perché anche se count++ sembra essere un’operazione atomica, NON lo è e causa la corruzione dei dati.
Sicurezza del thread in Java
La sicurezza del thread in Java è il processo per rendere il nostro programma sicuro da utilizzare in un ambiente multithread, ci sono diversi modi attraverso i quali possiamo rendere il nostro programma thread safe.
- La sincronizzazione è lo strumento più facile e ampiamente utilizzato per la sicurezza del thread in Java.
- Utilizzo delle classi wrapper atomiche del pacchetto java.util.concurrent.atomic. Ad esempio AtomicInteger
- Utilizzo dei blocchi del pacchetto java.util.concurrent.locks.
- Utilizzo di classi di collezione thread-safe, controlla questo post per l’uso di ConcurrentHashMap per la sicurezza del thread.
- Utilizzo della parola chiave volatile con le variabili per fare in modo che ogni thread legga i dati dalla memoria, non dalla cache del thread.
Java synchronized
La sincronizzazione è lo strumento con cui possiamo ottenere la thread-safety, la JVM garantisce che il codice sincronizzato verrà eseguito da un solo thread alla volta. La parola chiave sincronizzata di Java java keyword synchronized viene utilizzata per creare codice sincronizzato e internamente utilizza blocchi su Oggetti o Classi per assicurarsi che solo un thread stia eseguendo il codice sincronizzato.
- La sincronizzazione di Java funziona tramite il blocco e lo sblocco della risorsa prima che qualsiasi thread entri nel codice sincronizzato, deve acquisire il blocco sull’oggetto e quando l’esecuzione del codice termina, sblocca la risorsa che può essere bloccata da altri thread. Nel frattempo, gli altri thread si trovano in stato di attesa per bloccare la risorsa sincronizzata.
- Possiamo utilizzare la parola chiave synchronized in due modi: uno è rendere un metodo completo sincronizzato e l’altro è creare un blocco sincronizzato.
- Quando un metodo è sincronizzato, blocca l’oggetto, se il metodo è statico blocca la classe, quindi è sempre una buona pratica utilizzare un blocco sincronizzato per bloccare solo le sezioni del metodo che necessitano di sincronizzazione.
- Nel creare un blocco sincronizzato, è necessario fornire la risorsa su cui verrà acquisito il blocco, può essere XYZ.class o qualsiasi campo oggetto della classe.
synchronized(this)
bloccherà l’oggetto prima di entrare nel blocco sincronizzato.- Dovresti utilizzare il livello più basso di blocco, ad esempio, se ci sono più blocchi sincronizzati in una classe e uno di essi blocca l’oggetto, gli altri blocchi sincronizzati non saranno disponibili per l’esecuzione di altri thread. Quando si blocca un oggetto, si acquisisce un blocco su tutti i campi dell’oggetto.
- La sincronizzazione di Java garantisce l’integrità dei dati a costo delle prestazioni, quindi dovrebbe essere utilizzata solo quando è assolutamente necessario.
- La sincronizzazione di Java funziona solo nello stesso JVM, quindi se è necessario bloccare una risorsa in un ambiente con più JVM, non funzionerà e potrebbe essere necessario cercare un meccanismo di blocco globale.
- La sincronizzazione di Java potrebbe causare deadlock, controlla questo post su deadlock in java e come evitarli.
- La parola chiave sincronizzata di Java non può essere utilizzata per costruttori e variabili.
- È preferibile creare un falso oggetto privato da utilizzare per il blocco sincronizzato in modo che il suo riferimento non possa essere modificato da altri codici. Ad esempio, se hai un metodo setter per l’oggetto su cui stai sincronizzando, il suo riferimento può essere modificato da un altro codice che porta all’esecuzione parallela del blocco sincronizzato.
- Non dovremmo utilizzare alcun oggetto che viene mantenuto in una pool costante, ad esempio una Stringa non dovrebbe essere utilizzata per la sincronizzazione perché se un altro codice sta bloccando la stessa Stringa, cercherà di acquisire il blocco sullo stesso oggetto di riferimento dal pool delle Stringhe e anche se entrambi i codici non sono correlati, si bloccheranno a vicenda.
Ecco le modifiche al codice che dobbiamo fare nel programma precedente per renderlo thread-safe.
//oggetto fittizio per la sincronizzazione
private Object mutex=new Object();
...
//utilizzo di un blocco sincronizzato per leggere, incrementare e aggiornare il valore di count in modo sincrono
synchronized (mutex) {
count++;
}
Vediamo alcuni esempi di sincronizzazione e cosa possiamo imparare da loro.
public class MyObject {
//Blocca il monitor dell'oggetto
public synchronized void doSomething() {
// ...
}
}
//Codice degli hacker
MyObject myObject = new MyObject();
synchronized (myObject) {
while (true) {
//Ritardo indefinito di myObject
Thread.sleep(Integer.MAX_VALUE);
}
}
Si noti che il codice dell’hacker sta cercando di bloccare l’istanza di myObject e, una volta ottenuto il blocco, non lo rilascia mai, causando il blocco del metodo doSomething() in attesa del blocco. Ciò causerà il sistema di andare in deadlock e causare Denial of Service (DoS).
public class MyObject {
public Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// ...
}
}
}
// codice non attendibile
MyObject myObject = new MyObject();
// cambia il riferimento dell'oggetto di blocco
myObject.lock = new Object();
Si noti che l’oggetto di blocco è pubblico e cambiando il suo riferimento, possiamo eseguire il blocco sincronizzato in parallelo in più thread. Un caso simile è vero se si ha un oggetto privato ma si ha un metodo setter per cambiare il suo riferimento.
public class MyObject {
// si blocca sul monitor dell'oggetto della classe
public static synchronized void doSomething() {
// ...
}
}
// codice dell'hacker
synchronized (MyObject.class) {
while (true) {
Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
}
}
Si noti che il codice dell’hacker sta ottenendo un blocco sul monitor della classe e non lo rilascia, causando deadlock e DoS nel sistema. Ecco un altro esempio in cui più thread lavorano sulla stessa matrice di stringhe e, una volta elaborate, aggiungono il nome del thread al valore della matrice.
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();
// avvia tutti i thread
t1.start();t2.start();t3.start();
// attendi il completamento dei thread
t1.join();t2.join();t3.join();
System.out.println("Time taken= "+(System.currentTimeMillis()-start));
// controlla il valore della variabile condivisa ora
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++){
// elabora i dati e aggiungi il nome del thread
processSomething(i);
addThreadName(i, name);
}
}
private void addThreadName(int i, String name) {
strArr[i] = strArr[i] +":"+name;
}
private void processSomething(int index) {
// elaborazione di alcuni lavori
try {
Thread.sleep(index*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Ecco l’output quando eseguo il programma precedente.
Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]
I valori dell’array di stringhe sono corrotti a causa dei dati condivisi e della mancanza di sincronizzazione. Ecco come possiamo modificare il metodo addThreadName() per rendere il nostro programma thread-safe.
private Object lock = new Object();
private void addThreadName(int i, String name) {
synchronized(lock){
strArr[i] = strArr[i] +":"+name;
}
}
Dopo questa modifica, il nostro programma funziona correttamente ed ecco l’output corretto del 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]
{
“error”: “Upstream error…”
}
Source:
https://www.digitalocean.com/community/tutorials/thread-safety-in-java