Почему эта синхронизация не работает в данном сценарии? - PullRequest
1 голос
/ 07 февраля 2020
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();
    }
}
---------------------------------------------------------------------------

В подходе 3 он не дает одинаковый hashCode для 2 объектов, я держал и Thread.sleep, и экземпляр объекта в синхронизированном блоке, так что, как я думаю, 2-й поток даже не должен входить в этот блок до 1-го завершается, но он все еще делает и создает 2-й объект, ведущий к diff hashCode Что я здесь пишу? Может ли кто-нибудь исправить мое понимание здесь? Если я проверяю создание объекта null b4, тогда он работает как положено, но зачем мне здесь снова проверять null, потому что весь мой код находится в синхронизированном блоке?

if(null == singleton)
       singleton = new SingletonClass();

Ответы [ 3 ]

7 голосов
/ 07 февраля 2020

Вот один из способов, которым код (подход 3) в конечном итоге создает и возвращает два (или более) отдельных объекта для синглтона:

  • Поток A входит в функцию и видит null для singleton
  • Поток B входит в функцию и видит null для singleton
  • Поток A входит в синхронизированный блок
  • Поток B ожидает, потому что не может войти в синхронизированный block
  • Поток A присваивается singleton
  • Поток A выходит из синхронизированного блока
  • Поток A возвращает один объект
  • Поток B входит в синхронизированный блок
  • Поток B присваивает singleton
  • Поток B возвращает другой объект

Например, существует разрыв между проверкой null и входом в синхронизированный блок, который следует за ним.

Чтобы решить эту проблему, просто сделайте метод getInstance a synchronized и удалите блок synchronized внутри него:

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

Или, если вы действительно хотите избежать синхронизации при последующих вызовах Java 5 или более поздней версии ( используйте!), объявите singleton volatile и повторите проверку в блоке 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;
}

Это идиома с двойной проверкой . В Java 4 (он же 1.4) и ранее это не всегда было надежно, но теперь это так (если вы используете volatile для участника).

В комментарии пользователь2683814 задал хороший вопрос:

Не могли бы вы объяснить назначение локальной переменной перед проверкой нуля во втором фрагменте кода? Проверка переменной класса напрямую не будет работать?

Да, это будет работать, но менее эффективно.

В случаях, когда singleton не является null, используя local означает, что метод обращается только к singleton один раз . Если код не использует локальный код, он получит доступ к singleton как минимум дважды (один раз, чтобы проверить его, один раз, чтобы вернуть его). Поскольку доступ к переменной volatile немного дорог, лучше использовать локальную (которую в приведенном выше коде можно оптимизировать в регистр).

Это может показаться преждевременной микрооптимизацией, но если вы не Делая это в критически важном для кода коде, вы просто включили бы метод synchronized и полностью избежали бы сложности двойной проверки блокировки. : -)

1 голос
/ 07 февраля 2020

В третьем подходе вы проверяете переменную singleton; Вы делаете это вне любого синхронизированного блока, поэтому он не работает: здесь нет гарантии, что потоки ждут перед проверкой. Все они проверяют так быстро, как только могут, поэтому потоки 2+ могут все видеть здесь ноль, даже если один из них уже работает над созданием этого экземпляра.

Затем вы синхронизируете, конечно. Тем не менее, это волшебным образом не дает этому коду «назначать одноразовые однократные» полномочия - в конце концов, код в этом одноэлементном блоке собирается назначать вновь созданный экземпляр SingletonClass переменной singleton.

Два соответствующих примечания:

[1] Модель памяти java утверждает, что любое данное поле похоже на кошку Шредингера: каждый поток имеет его копию или не имеет - вплоть до модели потоков. Отдельная копия отправляется в копию каждого потока или в некоторые из них в произвольные моменты времени, и то же самое касается получения обновлений от других. Вы не можете полагаться на этот механизм, он может даже не использоваться, нет способа управлять им (кроме volatile, который может помочь, но его немного сложно использовать правильно). Дело в том, чтобы написать свой код так, чтобы он не имел значения. Как только вы устанавливаете sh отношения «до», «/ после» между кодами, например, из-за того, что вы используете синхронизированный блок, эта произвольная природа исчезает, и вам гарантирована видимость (поэтому, если код A предшествует коду B, например, поскольку они оба синхронизируются на одном и том же объекте и A «выиграли» битву, все, что A записывает в любом месте, будет видно B, как только B начнет работать, гарантировано, потому что здесь есть отношение CA / CB).

Поместите эту нулевую проверку внутрь, и проблема внезапно исчезнет.

[2] Если все, что вы пытаетесь сделать sh, это то, что существует ровно один экземпляр SingletonClass, вы лаять не на то дерево. Это не то, как это сделать. Это на самом деле тривиально просто. Все, что вы делаете, это одна строка:

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

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

Вот и все. Вы можете подумать, что это означает, что класс инициализируется при загрузке вашего приложения, но это не так. Классы загружаются, только если запущен какой-то код, который использует класс. Предполагая, что ВСЕ случаи использования SingletonClass включают получение этого экземпляра синглтона (обычно true), это так же хорошо, как и все остальное. Если по какой-то причудливой причине причина может взаимодействовать с S C без захвата синглтона, вы все равно можете использовать этот механизм, просто используя внутренний класс:

public class SingletonClass {
    private SingletonClass() {}

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

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

Это гарантированно не вызывает этот конструктор, пока кто-то вызывает getInstance (), вызывает только один раз, НЕ МОЖЕТ вызывать его дважды и делает это наиболее эффективным способом.

РЕДАКТИРОВАТЬ: Форматирование.

0 голосов
/ 07 февраля 2020

Таким образом, проблема в том, что оба потока могут достичь метода сразу, и поэтому оба получат объект как нулевой сразу перед первым входом в синхронизированный блок.

Используя комментарии @rzwitserloot и @TJ Crowder, я пришел к выводу, что Для создания объекта SIngleton не обязательно использовать синхронизированный. Приведенный ниже код может это сделать, и он также тестировал с помощью потоковых тестов и j-юнитов

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();
    }
}

Хэш-код одинаков для всех 4 потоков.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...