Implementare correttamente il pattern singleton in Java

Un pattern usatissimo (e soprattutto abusatissimo) nei linguaggi object oriented è il pattern Singleton, il quale in breve viene utilizzato quando si vuole essere sicuri che di una certa classe esista un’unica istanza. Lasciando da parte le discussioni sul perché sarebbe bene limitare al massimo l’uso di questo pattern, sul come sia sbagliato usarlo per raggruppare un insieme di funzioni “utility” e così via, occupiamoci della sua implementazione. Il metodo più ingenuo per implementarlo è il seguente:

// Implementazione lazy non thread safe
public class MySingleton {
  private static MySingleton instance;

  private MySingleton() {}

  public static MySingleton getInstance() {
    if (instance==null) {
      instance = new MySingleton();
    }
    return instance;
  }
}

Questa implementazione va benissimo finché l’applicazione è formata rigorosamente da un singolo thread. Nel caso in cui più thread possano richiamare il metodo getInstance() c’è una race condition: potrebbero entrambi superare il test dell’if e ottenere due istanze diverse della classe MySingleton.

La prima idea che viene per risolvere il problema è:

// Implementazione lazy corretta ma lenta
public class MySingleton {
  private static MySingleton instance;

  private MySingleton() {}

  public synchronized MySingleton getInstance() {
    if (instance==null) {
      instance = new MySingleton();
    }
    return instance;
  }
}

Questa implementazione è effetivamente thread safe, ma rende il metodo getInstance() sincronizzato, il che ha un certo impatto prestazionale, per cui sarebbe preferibile evitarla. C’è una soluzione semplice che però manca in laziness:

// Implementazione corretta non lazy
public class MySingleton {
  private final static MySingleton instance = new MySingleton();

  private MySingleton() {}

  public static MySingleton getInstance() {
    return instance;
  }
}

Questo codice è thread safe senza usare alcun costrutto particolare del linguaggio. L’unica sua pecca è che il riferimento all’istanza è inizializzato staticamente, per cui potrebbe istanziato anche molto prima della chiamata a getInstance() (basterebbe una chiamata a qualunque metodo statico di MySingleton per istanziarlo). Se nel programma considerato non interessa la laziness, questa può essere una buona soluzione, che ha anche il pregio di funzionare su tutte le versioni della JVM.

C’è una soluzione lazy apparentemente corretta detta double checked locking, molto in voga in passato, che però ha un subdolo bug che di fatto la rende thread unsafe:

// Implementazione non thread safe
public class MySingleton {
  private static MySingleton instance;

  private MySingleton() {}

  public static MySingleton getInstance() {
    if (instance==null) {
      synchronized(MySingleton.class) {
        if (instance==null) {
          instance = new MySingleton();
        }
      }
    return instance;
  }
}

Supponiamo di avere due thread, thread1 e thread2 che richiamano il metodogetInstance(): thread1 passa il test per entrambi gliif e inizia l’istanziazione di MySingleton, a questo punto thread2 esegue il primo if. Poiché non è entrato nella sezione sincronizzata non c’è una relazione “happens before” tra i due thread, e il comportamento del codice non è definito: thread1 potrebbe ricevere una istanza corretta, una non correttamente inizializzata o null (e istanzierebbe dunque un altro oggetto MySingleton, e il pattern è distrutto).

Con Java 5 è stato formalizzato il modello di memoria di Java, e è possibile modificare il codice precedente in modo che sia corretto anche se richiamato da più thread:

// Implementazione lazy thread safe *solo* per Java 5 e successivi
public class MySingleton {
  private volatile static MySingleton instance;

  private MySingleton() {}

  public static MySingleton getInstance() {
    if (instance==null) {
      synchronized(MySingleton.class) {
        if (instance==null) {
          instance = new MySingleton();
        }
      }
    return instance;
  }
}

La differenza è nella keyword volatile davanti alla variabile instance. Dichiarare una istanza come volatile significa che un aggiornamento della variabile stabilisce una relazione “happens before”. In un certo senso è come se i lettori e gli scrittori della variabile utilizzassero blocchi sincronizzati per cui il lettore ha la garanzia di vedere il valore correttamente aggiornato dallo scrittore.

Questo tipo di double checked locking è thread safe e probabilmente più veloce della versione che utilizza il metodo dichiarato come synchronized, anche se la differenza è probabilmente molto piccola. Questa versione non è garantito che funzioni correttamente con versioni di Java dalla 1.4 e precedenti (questo perché il memory model è stato formalizzato con Java 5), mentre è garantito funzionante dalla versione 5 in poi. Inoltre le variabili volatile danno delle penalizzazioni prestazionali su sistemi multi-CPU paragonabili a quelli della sincronizzazione.

Esiste una versione ancora più elegante, lazy, thread safe e funzionante su tutte le versioni di Java ideata da William Pugh:

// Implementazione lazy thread safe
public class MySingleton {
  private MySingleton() {}

  private static class MySingletonHolder {
    private final static MySingleton instance = new MySingleton();
  }

  public static MySingleton getInstance() {
    return MySingletonHolder.instance;
  }
}

Il “trucco” è nel fatto che la classe interna MySingletonHolder viene caricata dal class loader la prima volta che c’e n’è bisogno, vale a dire quando viene usata dal metodo getInstance(), per cui questo codice è thread safe senza usare costrutti particolari, è lazy perché non anticipa l’istanziazione del singleton e funziona su tutte le versioni di Java.

Il singleton è un pattern molto semplice, ma è sorprendentemente poco intuitivo realizzarne una implementazione corretta. A questo proposito Jon Bentley in Programming Pearls ricorda come la prima descrizione dell’algoritmo di ricerca binario (il binary search) sia stata pubblicata nel 1946, ma la sua prima implementazione corretta solo nel 1962. Come scriveva Donald Knuth:

Beware of bugs in the above code; I have only proved it correct, not tried it.

Leave a Reply