Статьи

Шаблон синглтон-дизайна — взгляд львиного глаза

Несколько дней назад, когда я был в моем родном городе, один из моих подчиненных из моего коллеги присутствовал на собеседовании в MNC и был серьезно ранен в ходе этого собеседования. Я имею в виду, что он не мог квалифицировать собеседование из-за сложных вопросов, заданных комиссией по собеседованию. Когда я вернулся в Бангалор, он поделился смущающей ситуацией, с которой он столкнулся во время технического интервью. Основываясь на его сегодняшнем опыте, я пишу эту статью о модели проектирования Singleton. Кстати, мой младший коллега имеет почти четыре года опыта работы на Java. Один из спорных вопросов, с которыми он столкнулся, был: « Что такое шаблон проектирования Singleton и как вы напишите надежный класс Singleton? Тем не менее, позвольте мне дать вам основные и критические описания шаблона проектирования Singleton, часто используемого во время разработки проекта / продукта.

Как вы знаете, шаблон проектирования Singleton относится к категории « Шаблон создания ». Основной принцип гласит, что в любой момент времени должен быть один и только один экземпляр класса, независимо от множественных вызовов класса. За этим принципом стоит много концепций и конструкций. Многие разработчики предполагают разные способы реализации Singleton в Java. Некоторым разработчикам этот дизайн вообще не нравится. Однако мы должны сосредоточиться на этом дизайне, и другие факторы для нас совершенно не имеют значения. Давайте проанализируем этот дизайн с разных точек зрения.

Технические детали

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
package com.ddlab.rnd.patterns;
public class SingletonType1
{
    private static SingletonType1 instance = null;
 
    private SingletonType1()
    {
        super();
    }
 
    public static SingletonType1 getInstance()
    {
        if( instance == null )
            instance = new SingletonType1();
        return instance;
    }
}

Чтобы использовать и реализовать, мы должны написать следующий код.

1
SingletonType1 instance = SingletonType1.getInstance();

Это нормально и кажется правильным. Если вы напишите вышеупомянутый код 10 или более раз, вы получите тот же экземпляр. Для проверки правильности вышеуказанной программы. Давайте сделаем базовый клинический тест. Создайте экземпляры вышеприведенного класса, вызвав код «SingletonType1.getInstance ()» и поместите все экземпляры в набор. Как известно, Set не допускает дублирование. Итак, в конце концов, если вы получаете размер набора 1, то это правильная реализация. Вы можете это также. Определенно вы получите результат как 1, размер набора. Теперь у нас возникает вопрос: можем ли мы сломать вышеуказанный дизайн? Можно ли создать несколько экземпляров определенного выше класса? ДА. Мы можем. Мы можем сломать вышеупомянутый дизайн, и мы можем создать несколько экземпляров. Как так ????????

Давайте посмотрим код ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.ddlab.rnd.patterns;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
 
public class TestSingletonType1
{
    public void createMultiInstances()
    {
        System.out.println("\n** MULTIPLE INSTANCES FROM SINGLETO **\n");
        /*
         * Using Reflection you can break singleton
         */
        try
        {
            Class clazz = Class.forName("com.ddlab.rnd.patterns.SingletonType1");
            Constructor constructor = clazz.getDeclaredConstructors()[0];
            constructor.setAccessible(true);
            SingletonType1 instance1 = (SingletonType1)constructor.newInstance(null);
            SingletonType1 instance2 = (SingletonType1)constructor.newInstance(null);
            SingletonType1 instance3 = (SingletonType1)constructor.newInstance(null);
 
            System.out.printf( "%-15s %-15s %n", "SERIAL NO", "MULTI INSTANCES");
            System.out.printf( "%-15s %-15s %n", "---------", "---------------");
 
            System.out.format("%-15s %-15s %n", "INSTANCE 1 ",instance1);
            System.out.format("%-15s %-15s %n", "INSTANCE 2 ",instance2);
            System.out.format("%-15s %-15s %n", "INSTANCE 3 ",instance3);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
 
    public void createMultiInstances1()
    {
        System.out.println("\n********* MULTIPLE INSTANCES FROM SINGLETON ********\n");
        /*
         * Using Reflection you can break singleton
         */
        try
        {
            Class clazz = Class.forName("com.ddlab.rnd.patterns.SingletonType1");
            Method method = clazz.getDeclaredMethods()[0];
            Field field = clazz.getDeclaredFields()[0];
            field.setAccessible(true);
 
            SingletonType1 instance1 = (SingletonType1)method.invoke(clazz, null);
            field.set(clazz, null);
            SingletonType1 instance2 = (SingletonType1)method.invoke(clazz, null);
            field.set(clazz, null);
            SingletonType1 instance3 = (SingletonType1)method.invoke(clazz, null);
 
            System.out.printf( "%-15s %-15s %n", "SERIAL NO", "MULTI INSTANCES");
            System.out.printf( "%-15s %-15s %n", "---------", "---------------");
 
            System.out.format("%-15s %-15s %n", "INSTANCE 1 ",instance1);
            System.out.format("%-15s %-15s %n", "INSTANCE 2 ",instance2);
            System.out.format("%-15s %-15s %n", "INSTANCE 3 ",instance3);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
 
    public void createInstances()
    {
        System.out.println("\n*********** SINGLE INSTANCES FROM SINGLETON ********\n");
        SingletonType1 instance1 = SingletonType1.getInstance();
        SingletonType1 instance2 = SingletonType1.getInstance();
        SingletonType1 instance3 = SingletonType1.getInstance();
 
        System.out.printf( "%-15s %-15s %n", "SERIAL NO", "INSTANCES");
        System.out.printf( "%-15s %-15s %n", "---------", "----------");
 
        System.out.format("%-15s %-15s %n", "INSTANCE 1 ",instance1);
        System.out.format("%-15s %-15s %n", "INSTANCE 2 ",instance2);
        System.out.format("%-15s %-15s %n", "INSTANCE 3 ",instance3);
    }
 
    public static void main(String[] args)
    {
        new TestSingletonType1().createInstances();
        new TestSingletonType1().createMultiInstances();
        new TestSingletonType1().createMultiInstances1();
    }
 
}

Если вы запустите вышеуказанную программу, вы сможете увидеть множество экземпляров определенного одноэлементного класса.

Однако мы узнали, что подход Singleton-конструктора Singleton может быть нарушен с помощью отражения. В приведенном выше случае мы можем создать экземпляр класса с закрытым конструктором, а также получить доступ к закрытому полю. Ох … Действительно … Вы действительно создали несколько экземпляров … Да, БОСС, я сделал, как вы думаете? Вы строите дизайн любым способом, но я могу сломаться. На самом деле это ранит чувства настоящего эмоционального разработчика, такого как я. OKKkkk. Сейчас я напишу очень эффективный код, чтобы вы не смогли его сломать. В самом деле ……..???????? Очень важно изучить механизм Java Relection, чтобы исследовать красоту JAVA.

Теперь давайте посмотрим, как написать лучший код, чтобы другие разработчики не смогли взломать с помощью отражения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.ddlab.rnd.patterns;
import java.lang.reflect.ReflectPermission;
import java.security.Permission;
 
public class SingletonType2
{
    static
    {
        getInstance();
    }
    private static SingletonType2 instance = null;
 
    private SingletonType2()
    {
        super();
        //Add the following piece of code so that it can not be invoked using relection
        System.setSecurityManager(new SecurityManager() {
            @Override
            public void checkPermission(Permission perm)
            {
                if (perm instanceof ReflectPermission )
                {
                    System.out.println("\nYes I will not allow you to create the instance using Reflection...\n");
                    throw new SecurityException();
                }
                else
                {
                    //Do nothing
                }
            }
 
        });
    }
 
    public static SingletonType2 getInstance()
    {
        if( instance == null )
            instance = new SingletonType2();
 
        return instance;
    }
}

Теперь это правда, ваша атака отражения не повлияет на приведенный выше код. Вы получите исключение, если будете использовать отражение для создания другого экземпляра. Пока вы можете подумать, что это может быть нарушено с помощью утилиты самоанализа Java. Вы также можете подумать, что мы не будем обращаться к конструктору, скорее, мы получим доступ к полю, и позже мы установим значение поля как нулевое, и снова мы вызовем поле. Это хорошая стратегия, но у вас ничего не получится, поскольку утилита для самоанализа — это еще один вид отражения. Поскольку мы не разрешаем рефлексию, вы не сможете создавать несколько экземпляров. Однако вы все равно можете использовать интроспекцию для вызова метода «getInstance ()», но вы получите тот же экземпляр. Таким образом, наша идея отражения и самоанализа может быть отброшена в этом контексте. Давайте думать по-другому, указать на сериализацию класса. Так что же будет, если мы захотим сериализовать ???? В приведенном выше классе вы не можете перейти к наследованию, так как конструктор является закрытым, для пуленепробиваемого механизма вы можете сделать класс финальным. Мы не можем сериализовать класс SingletonType2, так как он не реализует интерфейс Serializable, а также мы не разрешаем отражение. Однако мы не можем сериализовать класс, который не реализует интерфейс Serilizable. Однако иногда требуется сохранить объект Singleton на один день. В этом случае мы должны реализовать интерфейс Serializable в синглтон-классе. Теперь наш проект или продукт требует сериализации, и мы не будем использовать концепцию SecurityManager. Давайте изменим вышеуказанный класс.

Давайте посмотрим класс Singleton с интерфейсом Serializable.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package com.ddlab.rnd.patterns;
import java.io.Serializable;
 
public class SingletonType11 implements Serializable
{
    private static final long serialVersionUID = -4137189065490862968L;
    private static SingletonType11 instance = null;
 
    private SingletonType11()
    {
        super();
    }
 
    public static SingletonType11 getInstance()
    {
        if( instance == null )
            instance = new SingletonType11();
        return instance;
    }
}

Хорошо, мы сможем сериализовать вышеуказанный класс, но мы снова предоставляем хакеру возможность создать несколько экземпляров, и, таким образом, наша концепция снова разрушается. Давайте посмотрим, как мы можем снова сломать концепцию Singleton через сериализацию объектов. Давайте использовать написать небольшой класс, как это.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
package com.ddlab.rnd.patterns;
import java.io.Serializable;
 
public class BreakSingleton implements Serializable
{
    private static final long serialVersionUID = 5904306999023481976L;
 
    private SingletonType11 instance2 = SingletonType11.getInstance();
 
    public SingletonType11 getInstance2() {
        return instance2;
    }
 
    public void setInstance1(SingletonType11 instance2) {
        this.instance2 = instance2;
    }  
}

Давайте посмотрим на тестовый класс для вышеперечисленного.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.ddlab.rnd.patterns;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
 
public class TestBreakSingleton
{
    public static void main(String[] args) throws Exception
    {
        BreakSingleton bs = new BreakSingleton();
 
        OutputStream out = new FileOutputStream("data/a.ser");
        ObjectOutputStream oout = new ObjectOutputStream(out);
        oout.writeObject(bs);
        oout.flush();
        oout.close();
        out.flush();
        out.close();
 
        InputStream in = new FileInputStream("data/a.ser");
        ObjectInputStream oin = new ObjectInputStream(in);
        BreakSingleton bs1 = (BreakSingleton)oin.readObject();
        oin.close();
        in.close();
 
        System.out.println("Instance from Serialization :::"+bs1.getInstance2());
        System.out.println("Normal Instance :::"+SingletonType11.getInstance());
 
        InputStream in1 = new FileInputStream("data/a.ser");
        ObjectInputStream oin1 = new ObjectInputStream(in1);
        BreakSingleton bs2 = (BreakSingleton)oin1.readObject();
        oin1.close();
        in1.close();
        System.out.println("Another Instance from Serialization :::"+bs2.getInstance2());
    }
 
}

Если вы запустите вышеуказанную программу, вы получите следующий тип вывода.

1
2
3
4
5
Instance from Serialization :::com.ddlab.rnd.patterns.SingletonType11@2586db54
 
Normal Instance :::com.ddlab.rnd.patterns.SingletonType11@12276af2
 
Another Instance from Serialization :::com.ddlab.rnd.patterns.SingletonType11@38a97b0b

Итак, теперь у вас есть три разных экземпляра класса Singleton. Снова мы сталкиваемся с большой проблемой, имея несколько экземпляров. Есть ли способ, чтобы мы не дали хакеру возможность создать несколько экземпляров, но мы сможем сериализовать объект? Оооо, да, есть. Теперь давайте посмотрим модифицированный одноэлементный Java-класс, чтобы мы могли сериализовать объект, и в любой момент времени мы получим согласованный одноэлементный класс.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.ddlab.rnd.patterns;
import java.io.ObjectStreamException;
import java.io.Serializable;
 
public class SingletonType11 implements Serializable
{
    private static final long serialVersionUID = -4137189065490862968L;
    private static SingletonType11 instance = null;
 
    private SingletonType11()
    {
        super();
    }
 
    public static SingletonType11 getInstance()
    {
        if( instance == null )
            instance = new SingletonType11();
        return instance;
    }
 
    private Object readResolve() throws ObjectStreamException
    {
        return instance;
    }
 
    private Object writeReplace() throws ObjectStreamException
    {
        return instance;
    }
}

В этом подходе мы получим согласованный одноэлементный объект из сериализованного объекта и обычного вызова метода «getInstance ()». Однако все же мы можем создать несколько экземпляров, используя Reflection, и мы не можем предотвратить отражение, поскольку хотим сериализовать объект. В этом случае мы можем сделать запрос и договориться с разработчиками не использовать рефлексию только для того, чтобы избежать стратегии рефлексии. Разработчики пришли к соглашению не нарушать, используя рефлексию.

Как насчет многопоточности или использования синглтона в многопоточном приложении? Давайте посмотрим, что здесь происходит. Давайте посмотрим использование потока в случае класса Singleton.

Давайте рассмотрим первый класс Синглтона, который мы обсуждали ранее.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package com.ddlab.rnd.patterns;
public class SingletonType1
{
    private static SingletonType1 instance = null;
 
    private SingletonType1()
    {
        super();
    }
 
    public static SingletonType1 getInstance()
    {
        if( instance == null )
            instance = new SingletonType1();
 
        return instance;
    }
}

В основном этот подход называется ленивой инициализацией. В случае многопоточности мы можем получить несколько экземпляров, если мы не обработаем должным образом. Давайте посмотрим код ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.ddlab.rnd.patterns;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
 
class Thread1 extends Thread
{
    @Override
    public void run()
    {
        SingletonType1 instance = SingletonType1.getInstance();
        //      System.out.println("In Thread 1 - Singleton Instance ---->"+instance);
        TestSingletonType1_Thread.singletonSet.add(instance);
    }
}
 
class Thread2 extends Thread
{
    @Override
    public void run()
    {
        SingletonType1 instance = SingletonType1.getInstance();
        //      System.out.println("In Thread 2 - Singleton Instance ---->"+instance);
        TestSingletonType1_Thread.singletonSet.add(instance);
    }
}

Давайте посмотрим на тестовый класс, как его использовать.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public class TestSingletonType1_Thread
{
    private static Set singletonSet1 = new HashSet();
    public static Set singletonSet = Collections.synchronizedSet(singletonSet1);
 
    public static void main(String[] args)
    {
        //Singleton concept is broken here
        for( int i = 0 ; i < 100 ; i++ )
        {
            new Thread1().start();
            new Thread2().start();
 
            if( singletonSet.size() > 1 )
                break;
            else
                continue;
        }
        System.out.println(singletonSet);
    }
}

Если вы запустите вышеупомянутую программу много раз, вы получите различные экземпляры класса Singleton.

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

1
[com.ddlab.rnd.patterns.SingletonType1@60723d7c, com.ddlab.rnd.patterns.SingletonType1@6d9efb05, com.ddlab.rnd.patterns.SingletonType1@8dd20f6]

Так что делать ? Можем ли мы объявить переменную volatile, давайте посмотрим сейчас. Давайте иметь модифицированную программу.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
package com.ddlab.rnd.patterns;
public class SingletonType1
{
    private static volatile SingletonType1 instance = null;
 
    private SingletonType1()
    {
        super();
    }
 
    public static SingletonType1 getInstance()
    {
        if( instance == null )
            instance = new SingletonType1();
        return instance;
    }
}

После многократного запуска программы вы можете получить вот так.

1
[com.ddlab.rnd.patterns.SingletonType1@3f0ef90c, com.ddlab.rnd.patterns.SingletonType1@2e471e30]

Однако использование volatile не служит нашей цели. Все та же проблема, можем ли мы использовать синхронизированный метод, да, мы можем это сделать. Во многих случаях большинство разработчиков задают вопрос, связанный с использованием ключевого слова volatile и его использованием в Singleton. Менее опытные разработчики могут быть счастливы, если они получат единственный экземпляр вышеупомянутого класса при первом запуске на своей машине. У меня есть много разработчиков, которые оправдывают запуск этой программы на своем компьютере, и они также показали мне. Это правильно, потому что им повезло с первой попытки. Но я много раз запускаю эту программу на своем компьютере и говорю им, чтобы они не забывали правду. Теперь многие из них начинают модифицировать программу, используя «синхронизированное» ключевое слово java и спасатель. Давайте посмотрим, что происходит с этим ключевым словом.

Давайте посмотрим ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package com.ddlab.rnd.patterns;
public class SingletonType1
{
    private static volatile SingletonType1 instance = null;
 
    private SingletonType1()
    {
        super();
    }
 
    public static synchronized SingletonType1 getInstance()
    {
        if( instance == null )
            instance = new SingletonType1();
 
        return instance;
    }
}

Но будет проблема с производительностью. Однако, проанализировав его, вы поймете, что синхронизация требуется только для первого вызова метода. Последующие вызовы не требуют синхронизации. Поэтому не рекомендуется использовать ключевое слово «synchronized» для каждого вызова. В долгосрочной перспективе это может иметь горькое влияние на развитие вашего продукта / проекта. Чтобы повысить эффективность вышеупомянутой программы, используйте модификацию вышеуказанной программы другим способом. Мы не будем синхронизировать весь метод, а сделаем чувствительную область.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package com.ddlab.rnd.patterns;
public class SingletonType1
{
    private static volatile SingletonType1 instance = null;
 
    private SingletonType1()
    {
        super();
    }
 
    public static SingletonType1 getInstance()
    {
      if (instance == null)
      {
        synchronized(SingletonType1.class) {
          instance = new SingletonType1();
        }
      }
      return instance;
    }
}

Вышеупомянутая программа выглядит хорошо, и мы счастливы, и давайте праздновать сейчас. Но все же мы далеки от горькой реальности, поскольку существует очень большая проблема. Два потока могут попасть внутрь оператора if одновременно, когда экземпляр равен нулю. Затем один поток входит в синхронизированный блок для инициализации экземпляра, а другой блокируется. Когда первый поток выходит из синхронизированного блока, ожидающий поток входит и создает еще один объект Singleton. Обратите внимание, что когда второй поток входит в синхронизированный блок, он не проверяет, является ли экземпляр ненулевым. Давайте сделаем небольшой клинический тест, чтобы увидеть реальность.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.ddlab.rnd.patterns;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
 
class Thread11 extends Thread
{
    @Override
    public void run()
    {
        SingletonType111 instance = SingletonType111.getInstance();
        //      System.out.println("In Thread 1 - Singleton Instance ---->"+instance);
        TestSingletonType111_Thread.singletonSet.add(instance);
    }
}
 
class Thread22 extends Thread
{
    @Override
    public void run()
    {
        SingletonType111 instance = SingletonType111.getInstance();
        //      System.out.println("In Thread 2 - Singleton Instance ---->"+instance);
        TestSingletonType111_Thread.singletonSet.add(instance);
    }
}
 
public class TestSingletonType111_Thread
{
    private static Set singletonSet1 = new HashSet();
    public static Set singletonSet = Collections.synchronizedSet(singletonSet1);
 
    public static void main(String[] args)
    {
        //Singleton concept is broken here
        for( int i = 0 ; i < 100 ; i++ )
        {
            new Thread11().start();
            new Thread22().start();
 
            if( singletonSet.size() > 1 )
                break;
            else
                continue;
        }
        System.out.println(singletonSet);
    }
 
}

Теперь запустите вышеупомянутую программу много раз, вы узнаете. Что делать дальше.

Теперь давайте рассмотрим другую концепцию, называемую «двойная проверка блокировки», которая, похоже, известна множеству разработчиков. Многие разработчики применяют во многих случаях и считают наиболее надежной формой синглтона.

В программной инженерии блокировка с двойной проверкой (также известная как «оптимизация блокировки с двойной проверкой») — это схема разработки программного обеспечения, используемая для сокращения накладных расходов на получение блокировки путем предварительного тестирования критерия блокировки («подсказка блокировки») без фактического получения замок. Только если блокировка

проверка критерия указывает на то, что блокировка требуется, действительная логика блокировки продолжается. В большинстве технических интервью техническая комиссия ожидает этого ответа от кандидатов. Если кандидат сможет ответить на этот вопрос по своему вкусу, техническая группа будет счастлива, и он выберет кандидата. В наши дни это понятие приобрело большое значение, но я скажу, что техническая группа не имеет достаточного опыта по этому понятию. Давайте проанализируем это очень глубоко. Основная структура «двойной проверки блокировки» приведена ниже.

01
02
03
04
05
06
07
08
09
10
11
12
public static SingletonType1 getInstance()
{
  if (instance == null)
  {
    synchronized(SingletonType1.class// Mark - 1
    
      if (instance == null)          // Mark - 2
        instance = new SingletonType1();  // Mark - 3
    }
  }
  return instance;
}

Теория блокировки с двойной проверкой заключается в том, что вторая проверка в // Mark-2 делает невозможным создание двух разных объектов Singleton. Okkkkk …. Это может быть верно для однопоточного приложения. А как насчет мелкозернистого многопоточного приложения? Давайте посмотрим на последовательность ниже.

Поток 1 входит в метод getInstance ().

Поток 1 входит в синхронизированный блок в // Mark — 1, потому что экземпляр является нулевым.

Поток 1 вытесняется потоком 2.

Поток 2 входит в метод getInstance ().

Поток 2 пытается получить блокировку в // Mark-1, потому что экземпляр все еще нулевой. Однако, поскольку поток 1 удерживает блокировку, поток 2 блокируется в // Mark — 1.

Поток 2 вытесняется потоком 1.

Поток 1 выполняется и, поскольку экземпляр все еще равен нулю в // Mark-2, создает объект Singleton и назначает его ссылку на экземпляр.

Поток 1 выходит из синхронизированного блока и возвращает экземпляр из метода getInstance ().

Поток 1 вытесняется потоком 2.

Поток 2 получает блокировку в // Mark-1 и проверяет, является ли экземпляр пустым.

Поскольку экземпляр не равен NULL, второй объект Singleton не создается, а объект, созданный потоком 1, возвращается. Теория блокировки с двойной проверкой идеальна. К сожалению, реальность совершенно иная. Проблема с двойной проверкой блокировки состоит в том, что нет гарантии, что она будет работать на однопроцессорных или многопроцессорных компьютерах. Проблема сбоя двойной проверки блокировки связана не с ошибками реализации в JVM, а с текущей моделью памяти платформы Java. Модель памяти допускает так называемую «запись по порядку» и является основной причиной неудачи этой идиомы. Однако понятие «записи по порядку» выходит за рамки нашего обсуждения. Суть в том, что блокировку с двойной проверкой в ​​любой форме не следует использовать, поскольку вы не можете гарантировать, что она будет работать в любой реализации JVM. Как мы уже видели, хотя «двойная проверка блокировки» может работать, но она может неожиданно завершиться неудачей. Каково решение ?

Решение Билла Пью

(Извлечение из Википедии), исследователь компьютерных наук Университета Мэриленда Билл Пью (Bill Pugh) написал о проблемах кода, лежащих в основе шаблона Singleton при реализации в Java. Усилия Пью над «двойной проверкой блокировки» привели к изменениям в модели памяти Java в Java 5 и к тому, что обычно считается стандартным методом реализации Singletons в Java. Техника, известная как идиома инициализации по требованию, настолько ленива, насколько это возможно, и работает во всех известных версиях Java. Он использует языковые гарантии для инициализации класса и поэтому будет работать корректно во всех Java-совместимых компиляторах и виртуальных машинах. На вложенный класс ссылаются не раньше (и поэтому загружает не раньше загрузчик класса), чем в тот момент, когда вызывается getInstance (). Таким образом, это решение является поточно-ориентированным и не требует специальных языковых конструкций (то есть, volatile или синхронизированных).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class Singleton
{
        // Private constructor prevents instantiation from other classes
        private Singleton() { }
 
        /**
        * SingletonHolder is loaded on the first execution of Singleton.getInstance()
        * or the first access to SingletonHolder.INSTANCE, not before.
        */
        private static class SingletonHolder {
                public static final Singleton INSTANCE = new Singleton();
        }
 
        public static Singleton getInstance() {
                return SingletonHolder.INSTANCE;
        }
}

Вышеуказанное называется « идиома держателя инициализации по требованию ». Вышеуказанная структура конструкции Singleton является надежной в случае многопоточных приложений. Позвольте нам понять концепцию в деталях. Давайте рассмотрим ниже небольшой пример.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class Something
{
        private Something()
       {
        }
 
        private static class LazyHolder
       {
                public static final Something INSTANCE = new Something();
        }
 
        public static Something getInstance()
       {
                return LazyHolder.INSTANCE;
        }
}

Как это работает

Реализация опирается на четко определенную фазу инициализации выполнения в виртуальной машине Java (JVM); см. раздел 12.4 Спецификации языка Java (JLS) для деталей. Когда класс Something загружается JVM, класс проходит инициализацию. Поскольку у класса нет статических переменных для инициализации, инициализация завершается тривиально. Статическое определение класса LazyHolder внутри него не инициализируется, пока JVM не определит, что LazyHolder должен быть выполнен. Статический класс LazyHolder выполняется только тогда, когда статический метод getInstance вызывается для класса Something, и в первый раз, когда это происходит, JVM загрузит и инициализирует класс LazyHolder. Инициализация класса LazyHolder приводит к инициализации статической переменной INSTANCE путем выполнения (частного) конструктора для внешнего класса Something. Поскольку фаза инициализации класса гарантируется JLS последовательным, то есть не параллельным, дальнейшая синхронизация не требуется в статическом методе getInstance во время загрузки и инициализации. И поскольку фаза инициализации записывает статическую переменную INSTANCE в последовательную операцию, все последующие параллельные вызовы getInstance будут возвращать тот же правильно инициализированный INSTANCE без каких-либо дополнительных накладных расходов на синхронизацию.

Однако, используя концепцию паттерна «Инициализация держателя инициализации по требованию», мы можем получить одноэлементную конструкцию, безопасную для потока Снова возникает вопрос, можем ли мы размышлять рефлексивно. Да, мы можем сломать вышеупомянутое понятие, используя механизм отражения Java, который я уже упоминал. Теперь возникает вопрос, есть ли какой-либо другой подход для создания правильного подхода к проектированию синглтона? Да, есть другой, предложенный Джошуа Блохом (главный технический архитектор, Лаборатория инноваций Google и автор известной книги «Эффективная Java») .

1
2
3
4
5
6
7
8
9
package com.ddlab.rnd.patterns;
public enum SingletonType3
{
    INSTANCE;
    public void doSomething(String arg)
    {
        //... perform operation here ...
    }
}

Это единственный надежный подход к созданию одноэлементного класса, который является сериализуемым и полностью потокобезопасным, так как перечисления по умолчанию потоково-безопасны. Что касается отражения, используя наш вышеупомянутый нормальный подход отражения, вы не можете разбить одноэлементный объект, так как у него нет конструктора. Что касается сериализации, вы сможете сериализовать ее, но каждый раз вы будете получать один и тот же экземпляр. Итак, наконец, мы должны впитать этот современный подход к созданию класса дизайна Singleton. Однако многие разработчики не знают об этом.

Однако большинство интервьюеров не примут вышеуказанные два подхода, потому что для них это может стать новой концепцией. Вы можете сделать аргумент на основе JLS и ссылки на книги. Каждый день некоторые из моих юниоров, коллег и друзей жалуются, что во время интервью это самый сложный вопрос, с которым они обычно сталкиваются во время интервью. Каким бы способом они не ответили на вопрос, интервьюер никогда не будет удовлетворен, это потому, что большинство из них не знают подход enum в случае синглтон-дизайна. Если вы также столкнулись с той же проблемой, приведите пример Джошуа Блоха. Вы можете столкнуться с вопросом некоторых разработчиков или интервьюеров, что «у класса Singleton должен быть закрытый конструктор, и должен быть метод getInstance ()». Вы должны аргументировать, что подчеркивающее утверждение неверно и не является протоколом или каким-либо эмпирическим правилом. Это просто подход, который мы приняли в течение определенного периода времени. Основная концепция синглтона в любой момент времени, должен быть только один экземпляр, независимо от того, как вы пишете код. Если интервью постоянно спорит с вами, вы спрашиваете его, в чем проблема определения enum как подхода к синглтону. Робкий интервьюер может выдвинуть какой-то бессмысленный аргумент. Наконец, вы говорите ему, что в JDK 5 enum был написан Джошем Блохом и Нилом Гафтером. Если у разработчика или интервьюера есть мужество, он может отправить письмо этим великим архитекторам. Если высокомерный интервьюер все еще дает ложные аргументы, тогда преподайте ему урок: «Сэр, расскажите мне подход синглтона, который нельзя сломать. По крайней мере, я по-разному сломаю твой дизайн синглтона.

Тем не менее, мы не можем нарушить описанный выше подход синглтона с помощью enum, но мы можем взломать вышесказанное, написав код для создания более одного экземпляра. Код приведен ниже, не используйте приведенный ниже код для вашего коммерческого продукта. Это неприятный подход к разрушению синглтона. Давайте посмотрим код ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.ddlab.rnd.patterns;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import sun.reflect.ConstructorAccessor;
 
public class CrackEnumSingleton
{
    public static void main(String[] args)
    {
        Set set = new HashSet();
        try
        {
            SingletonType3 firstInstance = SingletonType3.INSTANCE;
            System.out.println(firstInstance.getClass() + " " + firstInstance + " = " + System.identityHashCode(firstInstance));
            set.add(firstInstance);
 
            Constructor constructor = SingletonType3.class.getDeclaredConstructors()[0];
            Method acquire = constructor.getClass().getDeclaredMethod("acquireConstructorAccessor");//"acquireConstructorAccessor" fields for cracking
            acquire.setAccessible(true);
            acquire.invoke(constructor);
 
            Method get = constructor.getClass().getDeclaredMethod("getConstructorAccessor");//"getConstructorAccessor" fields for cracking
            get.setAccessible(true);
            ConstructorAccessor invoke = (ConstructorAccessor) get.invoke(constructor);
            Object secondInstance = invoke.newInstance(new Object[] {null,1});
            System.out.println(secondInstance.getClass() + " " + secondInstance + " = " + System.identityHashCode(secondInstance));
            set.add(secondInstance);
 
            System.out.println("Total No of Singletons :::"+set.size());
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
 
}

Однако вышеприведенный подход — это просто методика обучения, но нигде не реализованная. В этом мире каждый объект имеет позитивный и негативный подход, но мы должны следовать позитивному подходу для беспрепятственного развития нашего продукта или проекта.

Вывод

Эта статья не имеет никакого коммерческого значения. В этой статье я только что представил лучший подход к написанию лучшего класса дизайна Singleton. Там могут быть лучшие подходы, если вы знаете какие-либо другие лучшие подходы или лучшие практики, пожалуйста, поделитесь со мной. Также предоставьте некоторые комментарии, чтобы мы могли внести больший вклад в лучший стандарт кодирования. Я надеюсь, вам понравится моя статья. В случае любой ошибки, пожалуйста, сообщите мне на [email protected] . Спасибо.