package singleton;

public class SingletonClass {

    private static SingletonClass singleton = null;

    private SingletonClass() {
    }

    static boolean stopThread = true;

    //approach 1 which fails in multithereaded env
    /*public static SingletonClass getInstance(){
        if(null == singleton){
            try {
                if(stopThread){
                    stopThread = false;
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            singleton = new SingletonClass();
        }
        return singleton;
    }*/

    //approach 2 which works
    //method is synchronized
   /* public static synchronized SingletonClass getInstance(){
        if(null == singleton){
                try {
                    if(stopThread){
                        stopThread = false;
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                singleton = new SingletonClass();

        }
        return singleton;
    }*/

    ***//approach 3 which is failing but I don't understand why
   //big block of code is synchronized
    public static SingletonClass getInstance(){
        if(null == singleton){
            synchronized (SingletonClass.class){
                try {
                    if(stopThread){
                        stopThread = false;
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                singleton = new SingletonClass();
            }
        }
        return singleton;
    }***


    //small block of code is synchronized, checked null again because even object instantiation is synchronised
    //if we don't check null, it will create new object once again
    //approach 4 which works
   /* public static SingletonClass getInstance(){
        if(null == singleton){
                try {
                    if(stopThread){
                        System.out.println("in thread...");
                        stopThread = false;
               //even if we interchange above 2 lines it makes whole lot of difference
               //till the time it takes to print "in thread"
               //2nd thread reaches there n enters if(stopThread) block because
               //stopThread is still true because 1st thread spent time in writing that sentence and 
               //did not set stopThread = false by the time 2nd thread reached there
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (SingletonClass.class){
                    System.out.println("in this block");
                    if(null == singleton){
                        singleton = new SingletonClass();
                    }
                }
        }
        return singleton;
    }*/

}


---------------------------------------------------------

package singleton;

public class ThreadUsage implements Runnable {

    @Override
    public void run() {
        SingletonClass singletonOne = SingletonClass.getInstance();
        System.out.println(singletonOne.hashCode());
    }
}

----------------------------------------------------------------

package singleton;

class ThreadUsageTest {

    public static void main(String[] args) {
        Runnable runnableOne = new ThreadUsage();
        Runnable runnableTwo = new ThreadUsage();
        new Thread(runnableOne).start();
        new Thread(runnableTwo).start();
    }
}
---------------------------------------------------------------------------

In Ansatz 3 wird nicht der gleiche Hashcode für 2 Objekte angegeben. Ich habe sowohl Thread.sleep als auch die Objektinstanziierung unter einem synchronisierten Block gehalten. Ich denke also, dass der 2. Thread diesen Block erst betreten sollte, wenn der 1. fertig ist Es wird immer noch ein zweites Objekt erstellt, das zu diff hashCode führt. Was mssing ich hier? Könnte jemand mein Verständnis hier korrigieren? Wenn ich nach null b4-Objekterstellung suche, funktioniert dies wie erwartet, aber warum sollte ich hier erneut null prüfen müssen, da sich mein gesamter Code unter einem synchronisierten Block befindet?

if(null == singleton)
       singleton = new SingletonClass();
1
Archana Mundaye 7 Feb. 2020 im 15:24

3 Antworten

Beste Antwort

Hier ist eine Möglichkeit, wie Code (Ansatz 3) zwei (oder mehr) separate Objekte für den Singleton erstellt und zurückgibt:

  • Thread A betritt die Funktion und sieht null für singleton
  • Thread B betritt die Funktion und sieht null für singleton
  • Thread A tritt in den synchronisierten Block ein
  • Thread B wartet, weil er den synchronisierten Block nicht betreten kann
  • Thread A weist singleton zu
  • Thread A verlässt den synchronisierten Block
  • Thread A gibt ein Objekt zurück
  • Thread B tritt in den synchronisierten Block ein
  • Thread B weist singleton zu
  • Thread B gibt ein anderes Objekt zurück

Beispielsweise besteht eine Lücke zwischen der Prüfung null und der Eingabe des darauf folgenden synchronisierten Blocks.

Um es zu lösen, machen Sie einfach getInstance zu einer synchronized Methode und entfernen Sie den synchronized Block darin:

public static synchronized SingletonClass getInstance() {
    if (instance == null) {
            singleton = new SingletonClass();
    }
    return singleton;
}

Oder wenn Sie die Synchronisierung bei nachfolgenden Aufrufen unter Java 5 oder höher (die Sie hoffentlich verwenden!) Wirklich vermeiden möchten, deklarieren Sie singleton volatile und überprüfen Sie es erneut im Block synchronized:

private static volatile SingletonClass singleton;
// ...
public static SingletonClass getInstance() { // Only works reliably on Java 5 (aka 1.5) and later!
    SingletonClass instance = singleton;
    if (instance == null) {
        synchronized (SingletonClass.class) {
            instance = singleton;
            if (instance == null) {
                singleton = instance = new SingletonClass();
            }
        }
    }
    return instance;
}

Das ist die doppelt überprüfte Sperrsprache . In Java 4 (auch bekannt als 1.4) und früher war dies nicht unbedingt zuverlässig, ist es aber jetzt (vorausgesetzt, Sie verwenden volatile für das Mitglied).

In einem Kommentar stellte user2683814 eine gute Frage:

Könnten Sie die Zuordnung zur lokalen Variablen vor der Nullprüfung im zweiten Codeausschnitt erläutern? Das direkte Überprüfen der Klassenvariablen funktioniert nicht?

Ja, es würde funktionieren, aber weniger effizient.

In Fällen, in denen singleton nicht - null ist, bedeutet die Verwendung eines lokalen Elements, dass die Methode nur einmal auf singleton zugreift. Wenn der Code keinen lokalen Code verwenden würde, würde er mindestens zweimal auf singleton zugreifen (einmal, um ihn zu überprüfen, einmal, um ihn zurückzugeben). Da der Zugriff auf eine volatile -Variable etwas teuer ist, ist es besser, die lokale Variable zu verwenden (die im obigen Code in ein Register optimiert werden kann).

Das mag wie eine vorzeitige Mikrooptimierung erscheinen, aber wenn Sie dies nicht in leistungskritischem Code tun würden, würden Sie einfach die Methode synchronized erstellen und die Komplexität der doppelt überprüften Sperrung vollständig vermeiden. :-)

7
T.J. Crowder 7 Feb. 2020 im 12:51

In Ansatz drei überprüfen Sie die Variable singleton. Sie tun dies außerhalb eines synchronisierten Blocks, weshalb dies nicht funktioniert: Es gibt hier keine Garantie dafür, dass Threads vor der Überprüfung warten. Sie alle überprüfen so schnell sie können, weshalb 2+ Threads hier möglicherweise alle eine Null sehen, selbst wenn einer von ihnen bereits daran arbeitet, diese Instanz zu erstellen.

Sie synchronisieren dann sicher. Dies gibt diesem Code jedoch nicht auf magische Weise die Befugnis, Singleton nur einmal zuzuweisen. Schließlich wird der Code in diesem Singleton-Block der Variablen singleton eine neu erstellte Instanz von SingletonClass zuweisen.

Zwei relevante Hinweise:

[1] Das Java-Speichermodell gibt an, dass ein bestimmtes Feld wie Schrödingers Katze ist: Jeder Thread hat eine Kopie davon oder nicht - bis zum Threading-Modell. Eine einzelne Kopie wird zu beliebigen Zeiten an die Kopie des jeweils anderen Threads oder an einige von ihnen gesendet, und das Gleiche gilt für den Empfang von Aktualisierungen von den anderen. Sie können sich nicht auf diesen Mechanismus verlassen, er wird möglicherweise nicht einmal verwendet, es gibt keine Möglichkeit, ihn zu steuern (außer volatile, was helfen kann, aber es ist etwas schwierig, ihn richtig zu verwenden). Der Punkt ist, Ihren Code so zu schreiben, dass es keine Rolle spielt. Sobald Sie 'Kommt vor' / 'Kommt nach' -Beziehungen zwischen Code hergestellt haben, z. B. weil Sie einen synchronisierten Block verwenden, verschwindet diese willkürliche Natur und Sie haben garantierte Sichtbarkeit (wenn also Code A vor Code B kommt, z. B. weil beide Wenn Sie auf demselben Objekt synchronisieren und A den Kampf "gewonnen" hat, ist alles, was A irgendwo schreibt, für B sichtbar, sobald B ausgeführt wird (garantiert, da hier eine CA / CB-Beziehung besteht).

Setzen Sie diesen Null-Check ein und plötzlich verschwindet das Problem.

[2] Wenn Sie nur versuchen, genau eine Instanz von SingletonClass zu erreichen, bellen Sie den falschen Baum an. So geht das nicht. Es ist in der Tat TRIVIALLY einfach. Alles was Sie tun, ist diese eine Zeile:

public class SingletonClass {
    public static final SingletonClass instance = new SingletonClass();

    private SingletonClass() {
        // ensure nobody but you can call this.
    }
}

Das ist es. Sie denken vielleicht, dass dies bedeutet, dass die Klasse beim Booten Ihrer App initialisiert wird, aber das stimmt nicht. Klassen werden nur geladen, wenn Code ausgeführt wird, der die Klasse verwendet. Angenommen, bei ALLEN Verwendungen von SingletonClass wird diese Singleton-Instanz abgerufen (normalerweise wahr). Dies ist so gut wie alles andere. Wenn aus irgendeinem bizarren Grund Code mit SC interagieren kann, ohne den Singleton zu greifen, können Sie diesen Mechanismus trotzdem verwenden, indem Sie einfach eine innere Klasse verwenden:

public class SingletonClass {
    private SingletonClass() {}

    public static SingletonClass getInstance() {
        return Inner.instance;
    }

    private static class Instance {
        private static final SingletonClass instance = new SingletonClass();
    }
}

Diese Garantie ruft diesen Konstruktor erst auf, wenn jemand getInstance () aufruft, ruft ihn nur einmal auf, kann ihn niemals zweimal aufrufen und tut dies auf die effizienteste Art und Weise.

BEARBEITEN: Formatieren.

1
rzwitserloot 7 Feb. 2020 im 12:33

Das Problem war also, dass beide Threads die Methode gleichzeitig erreichen können und beide das Objekt sofort als null erhalten, bevor der erste in den synchronisierten Block eintritt.

Mit @rzwitserloot und @ T.J. Crowder-Kommentare, ich bin zu dem Schluss gekommen, dass es nicht notwendig ist, synchronisiert zum Erstellen eines SIngleton-Objekts zu verwenden. Der folgende Code kann dies und wurde auch mit Thread-Tests in Junits getestet

package singleton;

public class SingletonClassSecond {

    private static SingletonClassSecond singleton = new SingletonClassSecond();

    private SingletonClassSecond() {
    }

    public static SingletonClassSecond getInstance(){
        return singleton;
    }
}

---------------------------------------------------------------------------

package singleton;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class SingletonClassTest {

    @Test
    public void shouldCreateSingleton(){
        SingletonClass singletonOne = SingletonClass.getInstance();
        SingletonClass singletonTwo = SingletonClass.getInstance();
        singletonOne.print("1");
        singletonTwo.print("2");
        Assertions.assertEquals(singletonOne.hashCode(),singletonTwo.hashCode());
    }

}

--------------------------------------------------------------------------------

package singleton;

class ThreadUsageTest {
    public static void main(String[] args) {
        Runnable runnable = new ThreadUsageSecond();
        Runnable runnableTwo = new ThreadUsageSecond();
        Runnable runnableThree = new ThreadUsageSecond();
        Runnable runnableFour = new ThreadUsageSecond();
        new Thread(runnable).start();
        new Thread(runnableTwo).start();
        new Thread(runnableThree).start();
        new Thread(runnableFour).start();
    }
}

Der Hashcode ist für alle 4 Threads gleich.

0
Archana Mundaye 7 Feb. 2020 im 19:38