Home > java > Hacking dei Singleton

Hacking dei Singleton

8 ottobre 2010

Translate in English with Google Translate

In un precedente articolo ho mostrato come era possibile creare un Singleton in Java. Con la versione Java 1.5 sono state introdotte le enum per semplificare e risolvere alcuni problemi che erano spesso riscontrati. Difatti l’utilizzo dei Singleton ha dato luogo ad un lungo dibattito in merito ai problemi ad essi collegati e non meno alla possibilità di ottenere multiple instanze sfruttando semplici trucchi.
In questo articolo vedremo come generare multiple instanze di un Singleton utilizzando diversi modalità:

  1. sfruttare il Multi-Threading
  2. usare la Reflection
  3. usare diversi ClassLoader
  4. usare la Serializzazione


1.Multi-Threading
Nel momento in cui ci imbattiamo in una applicazione che utilizza il multi-threading, dobbiamo sapere come comportarci per gestire la concorrenza. In poche parole dobbiamo essere confidenti con il concetto della sincronizzazione dei metodi, del lock e del monitor degli oggetti.

Cerco di riepilogare in poche parole un argomento che richiederebbe un libro intero:

  1. Ogni oggetto Java dispone di un stato interno di lock che viene attivato quando viene utilizzato il modificatore “synchronized” nella dichiarazione oppure all’interno del metodo stesso
  2. un metodo statico ha una sola instanza pertanto la sincronizzazione ci assicura l’accesso di un Thread alla volta, quindi gli altri Thread saranno in attesa ed accederanno al metodo solo quando viene rilasciato il lock
  3. un metodo di instanza può presentare molte instanze pertanto la sincronizzazione ci garantisce la concorrenza nel caso di accesso alla stessa instanza ad opera di più Thread, ma Thread diversi possono accedere simultaneamente a metodi sincronizzati di instanze diverse.
  4. la schedulazione dei Thread avviene ad opera della JVM e non è predicibile, a meno che venga stabilità una priorità di esecuzione
  5. la schedulazione dei Thread della JVM deve essere schedulata nei Thread del sistema operativo ed a seconda dell’algoritmo di schedulazione del kernel utilizzato dal sistema operativo, vi potrà essere un diverso comportamento del Thread in esame.

Nel nostro caso, l’esempio iniziale prevedeva la creazione di un metodo statico non sincronizzato, questo significa che qualora il metodo venga invocato simultaneamente da più Thread sarà consentito l’accesso concorrente al metodo, quindi due o più Thread possono dar luogo alla generazione di diversi oggetti, pertanto il Singleton viene violato.

Questa situazione avviene se si presentano queste condizioni:

  1. il Singleton viene invocato contemporaneamente da 2 o più Thread
  2. il metodo statico non è sincronizzato

Facciamo un esempio di una invocazione multi-Threading:

public class Start extends Thread  {

    public static void main(String[] args) {
        Thread t1 = new Thread( new Start() );
        Thread t2 = new Thread( new Start() );
        t1.start();
        t2.start();
    }

    @Override
    public void run(){
        MySingleton m = MySingleton.getInstance();
    }

}

Solitamente l’esecuzione del costruttore del Singleton richiede un tempo variabile in base alle operazioni che dovranno essere effettuate per l’inizializzazione del Singleton. Per rappresentare lo stato di concorrenza dobbiamo simulare un tempo di elaborazione quindi introduciamo una latenza usando il metodo sleep() della classe Thread e gestiamo l’eccezione InterruptedException:

public class MySingleton {
    private static MySingleton mySingleton = null;

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

    private MySingleton()  {
        System.out.println("Singleton instanziato! hashcode: " + hashCode() );
        try {
             Thread.sleep(1000);
        } catch (InterruptedException ex) {}
    }

}

Quando eseguiamo la classe Start abbiamo l’output seguente:

Singleton instanziato! hashcode: 7214088
Singleton instanziata! hashcode: 23660326

Abbiamo generato due instanze, quindi se il metodo statico non è sincronizzato possiamo generare 2 instanze diverse del Singleton approfittando dell’assenza di lock e giocando sul tempo di esecuzione del metodo.
Se l’esecuzione del metodo non sincornizzato è estremamente rapido, pertanto verranno effettuare poche o nessuna operazione, la possibilità di un accesso concorrente dimunuisce notevolmente, ma non si annulla.
2.Usare la Reflection
L’uso della reflection ci consente in un modo abbastanza semplice di cambiare i modificatori delle proprietà, dei metodi e del costruttore. Questa operazione ci apre le porte ad un accesso diretto pertanto, avendo a disposizione l’accesso, possiamo moltiplicare le instanze a nostro piacimento.
Vediamo come accedere al costruttore privato del Singleton ed instanziarlo una nuova volta:

public class Start {
    public static void main(String[] args) throws Exception {
        MySingleton.getInstance();
        Constructor constructor[] = MySingleton.class.getDeclaredConstructors();
        constructor[0].setAccessible(true);
        constructor[0].newInstance();
    }
}

L’output è il seguente:

Singleton instanziato! hashcode: 7214088
Singleton instanziato! hashcode: 23660326

Quindi abbiamo superato la barriera del costruttore!
Adesso facciamo qualcosa di simile ma cambiando il modificatore della proprietà che instanzia il Singleton da private a public, poi assegnamo il suo valore a null. Questo ci permetterà di resettare il Singleton e re-inizializzarlo.
Vediamo come:

public class Start {
    public static void main(String[] args) throws Exception {
        MySingleton.getInstance();
        Field f = MySingleton.class.getDeclaredField("mySingleton");
        f.setAccessible(true);
        f.set(null, null);
        MySingleton.getInstance();
    }
}

L’output è il seguente:

Singleton instanziato! hashcode: 8152936
Singleton instanziato! hashcode: 29293232

Questo è il modo più facile, ma i problemi ci possono essere quando viene attivato il SecurityManager che ci rende la vita impossibile! Ma lo vediamo un altra volta.

3.Usare diversi ClassLoader
Attraverso l’uso del Classloader è possibile duplicare il Singleton. Per fare ciò occorre creare una classe che estenda la classe astratta ClassLoader e carichi il Singleton, per esempio leggiamo tramite un FileInputStream il Singleton per poi convertirlo in oggetto Class da utilizzare.
Questo darà luogo ad una nuovo ClassLoader che verrà utilizzato per il caricamento del Singleton. In questo modo il primo Singleton sarà caricato con il ClassLoader standard delle applicazioni, mentre il secondo Singleton sarà caricato con il nostro ClassLoader.
Vediamo per prima il nostro “nuovo” ClassLoader:

import java.io.File;
import java.io.FileInputStream;

public class MyClassLoader extends ClassLoader {

    public Class loadClass(File classFile) throws ClassNotFoundException {
        byte[] classBytes = null;
        Class result = null;
        try {
            FileInputStream fi = new FileInputStream(classFile);
            classBytes = new byte[fi.available()];
            fi.read(classBytes);
            fi.close();
        } catch (Exception e) {}
        result = defineClass(classBytes, 0, classBytes.length);
        super.resolveClass(result);
        return result;
    }
}

Questa è la classe Start che instanzia il primo Singleton con il ClassLoader di default, poi instanza il secondo Singleton con il “nostro” ClassLoader:

public class Start  {
    public static void main(String[] args) throws Exception {
        MySingleton ms1 = MySingleton.getInstance();
        System.out.println( "ClassLoader: " + ms1.getClass().getClassLoader() );

        MyClassLoader myClassLoader = new MyClassLoader();
        File classFile = new File( "/single/MySingleton.class");
        Object obj = myClassLoader.loadClass(classFile).getDeclaredMethod("getInstance").invoke(null);
        System.out.println( "ClassLoader: " + obj.getClass().getClassLoader() );
    }
}

L’output sarà l seguente:

Singleton instanziato! hashcode: 8152936
ClassLoader: sun.misc.Launcher$AppClassLoader@198dfaf
Singleton instanziato! hashcode: 20732290
ClassLoader: single.MyClassLoader@5483cd

Ed ecco che abbiamo generato 2 istanze del Singleton!

4.Usare la serializzazione
Un altro modo per duplicare il Singleton è quella di usare la serializzazione. Ma di che si tratta? In pratica la serializzazione nasce dall’esigenza di memorizzare lo stato di un oggetto, poi tutto il suo contenuto viene trasferito su files, database, memoria oppure passato tramite la rete ad una JVM remota. A questo punto quando l’oggetto trasferito viene riportato in vita in una JVM avremo un oggetto identico a quello originario in termini di valori ma diverso in termini di oggetto, tanto per capirci l’hashcode sarà diverso quindi abbiamo un duplicato!
C’è solo un piccolo particolare, per serializzare un oggetto occorre che questo implementi l’interfaccia Serializzable, pertanto il nostro Singleton deve aver implementato tale interfaccia altrimenti, in sede di serializzazione, verrà rilanciata l’eccezione java.io.NotSerializableException. La cosa buffa è che l’interfaccia Serializzable è vuota! Se aprite il sorgente troverte questo:

package java.io;
public interface Serializable {
}

Per i meno esperti sembrerà uno scherzo. Ma in realtà dietro a questa scelta c’è un discorso di appartenenza di tipi, cioè il nostro Singleton se implementa Serializable dichiara la volontà di essere serializzato e pertanto si impegna a rispettare le regole della serializzazione, per esempio che tutti gli oggetti in essa contenuti siano anch’essi serializzabili.

Passiamo alla classe Start, ossia a quella che dovrà fare il lavoro sporco, in altri termini duplicare i Singleton.
Ho preparato un esempio in cui mostro le semplici operazioni di serializzazione e de-serializzazione sul Singleton.

public class Start {
    public static void main(String[] args) throws Exception {
        MySingleton m = MySingleton.getInstance();

        ByteArrayOutputStream fos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(m);
        oos.close();

        ByteArrayInputStream fis = new ByteArrayInputStream( fos.toByteArray() );
        ObjectInputStream ois = new ObjectInputStream( fis );
        MySingleton m1 = (MySingleton)ois.readObject();
        ois.close();

        System.out.println("Oggetto m1 serializzato! hashcode: " + m1.hashCode());
    }
}

L’output è questo:

Singleton instanziato! hashcode: 7214088
Oggetto m1 serializzato! hashcode: 29596205

L’output mostra un hashcode diverso per i 2 oggetti, quindi abbiamo riprodotto la duplicazione.
I più attenti hanno notato che “Singleton instanziato!” è stata scritta 1 sola volta. Questa stringa è presente nel costruttore del Singleton quindi è stata scritta durante la prima costruzione dell’oggetto. Quando l’oggetto viene de-serializzato il costruttore non viene richiamato e questo fa parte della procedura di serializzazione che non deve inizializzare nuovamente l’oggetto ma solo assegnare i valori salvati in sede di serializzazione.

Per evitare che ciò possa avvenire sarebbe stato consigliato inserire nel Singleton un metodo, questo:

private Object readResolve() {
    return mySingleton;
}

In questo modo, ogni qual volta viene invocato il metodo readObject() sull’oggetto ObjectInputStream, viene restituito la stessa istanza dell’oggetto statico del Singleton, altrimenti viene creata una nuova istanza del Singleton, come sopra.

Categorie:java
  1. Francesco
    29 luglio 2013 alle 8:32 PM

    Articolo straordinario! Mi spiace solo di averlo letto con quasi 3 anni di ritardo😉
    In effetti l’hacking è sfizioso proprio per questo: stimola parecchio, costringendoti a ragionare più approfonditamente.

  1. 9 maggio 2013 alle 6:04 PM
I commenti sono chiusi.
%d blogger cliccano Mi Piace per questo: