Статьи

Шаблон проектирования Java Singleton

Это один из самых простых шаблонов проектирования в Java.

Если кто-нибудь спросит меня, какой шаблон дизайна вам подходит, я с гордостью скажу «Синглтон».

Но когда они спрашивают в глубине концепции синглтона, я в тупик.

Это действительно синглтон, это так сложно?

На самом деле нет, но у него есть много сценариев, которые нам нужно понять (особенно начинающим).

Определение:

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

Определение так же просто, как 1,2,3 и A, B, C, D.

Давайте посмотрим, как мы можем реализовать Singleton Class.

Как мы можем гарантировать, что объект должен быть только один все время?

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

Логика создания объекта -> что это такое
Как мы создаем объект в Java?

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

Так как же мы можем гарантировать, что конструктор доступен и выполним только один раз?

  1. Запретить доступ конструктора вне класса, чтобы посторонние не смогли создать экземпляр.
    Как это сделать -> как предотвратить доступ к методу вне класса?
    Простой, сделать метод как частное право, аналогично сделать конструктор как частный.
  2. Предотвратите выполнение конструктора внутри класса более одного раза.
    Как это сделать -> это можно реализовать многими способами, давайте рассмотрим это на примере.

Если выше 2 условия выполнены, то у нас всегда будет один объект для нашего класса. И этот класс называется Singleton, так как он создает один объект все время, когда мы запрашиваем.

Не много теории, мы начнем реализовывать это сейчас.

Доступно много способов создания одноэлементного объекта:

Подход 1

  • Стремитесь инициализации или инициализации перед использованием
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
package com.kb.singleton;
 
public class EagerSingletonClass {
    private static volatile EagerSingletonClass singletonInstance = new EagerSingletonClass();
     
    //making constructor as private to prevent access to outsiders
    private EagerSingletonClass() {
         
    }
     
    public static EagerSingletonClass getInstance(){
        return singletonInstance;
    }
 
}

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

  • Тестовый класс Junit для вышеуказанного класса для тестирования синглтона.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
package com.kb.singleton;
 
import static org.junit.Assert.*;
 
import org.junit.Test;
 
public class EagerSingletonClassTest {
 
    @Test
    public void testSingleton() {
         
        EagerSingletonClass instance1 = EagerSingletonClass.getInstance();
        EagerSingletonClass instance2 = EagerSingletonClass.getInstance();
        System.out.println("checking singleton objects equality");
        assertEquals(true, instance1==instance2);
         
    }
 
}

Преимущество:
Эта стратегия создает объект во время загрузки класса и, следовательно, быстрее и безопаснее сценария многопоточности. Единственное, что мы должны сделать экземпляром — как volatile для обработки многопоточного сценария.

Недостаток :

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

Когда использовать вышеуказанную стратегию?
Всякий раз, когда мы на 100% уверены, что этот объект определенно используется в нашем приложении.
ИЛИ ЖЕ
Когда объект не тяжелый, то все в порядке, мы можем управлять скоростью и памятью.

Подход 2

  • Ленивая инициализация или инициализация, как и когда нам нужно

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.kb.singleton;
 
public class LazySingleton {
    private static volatile LazySingleton singletonInstance = null;
     
    //making constructor as private to prevent access to outsiders
    private LazySingleton() {
         
    }
     
    public static LazySingleton getInstance(){
        if(singletonInstance==null){
            synchronized (LazySingleton.class) {
                singletonInstance = new LazySingleton();
            }
        }
        return singletonInstance;
    }
 
 
 
}

В приведенной выше программе мы создали объект только при наличии запроса через метод getInstance ().

Здесь во время первого вызова getInstance () объект ‘singletonInstance’ будет иметь значение null, и он выполняет блок условия if, когда он становится истинным, и создает объект.

Затем последующие вызовы метода getInstance () вернут тот же объект.

Но если мы посмотрим на сценарий многопоточности, проблема возникает, когда ниже контекста приходит 2 потока, t1 и t2 вызывают метод getInstance (), а поток t1 выполняется if (singletonInstance == null) и находит singletonInstance как ноль, поэтому он входит в синхронизированный блок для создания объект.

Но прежде чем он выполнит логику создания объекта, если поток t2 выполняется if (singletonInstance == null), он также найдет singletonInstance как ноль, поэтому он также попытается ввести синхронизированный блок, но у него не будет блокировки, так как первый поток t1 уже вошел ,

Таким образом, поток t2 ожидает, пока поток t1 завершит выполнение синхронизированного блока.

Следовательно, поток t1 выполняется и создает объект. теперь поток t2 также входит в синхронизированный блок, так как ожидал синхронизированный блок, и снова создает объект.

Таким образом, два объекта создаются двумя потоками. Так не удается добиться синглтона.

Решением вышеуказанной проблемы является двойная проверка блокировки.

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

Таким образом, мы можем избежать создания объекта более одного раза несколькими потоками.

Как ?

Поток t1 проверяет условие if (singletonInstance == null), и оно в первый раз верно, поэтому он входит в синхронизированный блок, и там снова проверяется условие if (singletonInstance == null), и это также верно, поэтому создает объект.

Теперь поток t2 входит в метод getInstance () и предполагает, что он выполнил условие if (singletonInstance == null) до того, как поток t1 выполнит логику создания объекта, тогда t2 также ожидает входа в синхронизированный блок.

После того, как поток t1 выходит из синхронизированного блока, поток t2 входит в тот же блок, но у нас снова есть условие if, если if (singletonInstance == null), но поток t1 уже создал объект, это делает условие ложным и далее останавливает выполнение и возвращает тот же экземпляр.

Давайте посмотрим, как это можно сделать в коде:

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
package com.kb.singleton;
 
public class LazySingletonDoubleLockCheck {
 
    private static volatile LazySingletonDoubleLockCheck singletonInstance = null;
     
    //making constructor as private to prevent access to outsiders
    private LazySingletonDoubleLockCheck() {
         
    }
     
    public static LazySingletonDoubleLockCheck getInstance(){
        if(singletonInstance==null){
            synchronized (LazySingleton.class) {
                if(singletonInstance ==null){
                singletonInstance = new LazySingletonDoubleLockCheck();
                }
            }
        }
        return singletonInstance;
    }
 
 
 
 
 
}

Давайте проведем юнит-тестирование

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
package com.kb.singleton;
 
import static org.junit.Assert.*;
 
import org.junit.Test;
 
public class LazySingletonDoubleLockCheckTest {
 
    @Test
    public void testSingleton() {
         
        LazySingletonDoubleLockCheck instance1 = LazySingletonDoubleLockCheck.getInstance();
        LazySingletonDoubleLockCheck instance2 = LazySingletonDoubleLockCheck.getInstance();
        System.out.println("checking singleton objects equality");
        assertEquals(true, instance1==instance2);
        //fail("Not yet implemented");
    }
 
}

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

Подход 3

  • Синглтон используя Inner class

Давайте посмотрим на приведенный ниже код создания объекта с использованием внутреннего класса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package com.kb.singleton;
 
public class SingletonUsingInnerClass {
     
    private SingletonUsingInnerClass() {
         
    }
     
    private static class LazySingleton{
        private static final SingletonUsingInnerClass  SINGLETONINSTANCE = new SingletonUsingInnerClass();
    }
     
    public static SingletonUsingInnerClass getInstance(){
        return LazySingleton.SINGLETONINSTANCE;
    }
     
 
}

Код модульного теста

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package com.kb.singleton;
 
import static org.junit.Assert.*;
 
import org.junit.Test;
 
public class SingletonUsingInnerClassTest {
 
    @Test
    public void testSingleton() {
         
        SingletonUsingInnerClass instance1 = SingletonUsingInnerClass.getInstance();
        SingletonUsingInnerClass instance2 = SingletonUsingInnerClass.getInstance();
        System.out.println("checking singleton objects equality");
        assertEquals(true, instance1==instance2);
    }
 
}

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

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

Так что это также обеспечит создание объекта как и когда это потребуется. И это очень просто реализовать. Это также безопасно от многопоточности.

Подход 4

  • Синглтон с сериализацией и де сериализацией

Теперь предположим, что наше приложение распределено, и мы сериализуем наш одноэлементный объект и записываем его в файл. Позже мы читаем это путем де-сериализации одноэлементного объекта. При сериализации объекта всегда создается новый объект с состоянием, доступным внутри файла. Если мы сделаем какое-либо изменение состояния после записи в файл и затем попытаемся сериализовать объект, мы получим исходный объект, а не новый объект состояния. Таким образом, мы получили 2 объекта в этом процессе.

Давайте попробуем понять эту проблему программой:

Первое -> сделать сериализуемый класс 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
package com.kb.singleton;
 
import java.io.Serializable;
 
public class SingletonSerializeAndDesrialize implements Serializable {
     
    private int x=100;
     
    private static volatile SingletonSerializeAndDesrialize singletonInstance = new SingletonSerializeAndDesrialize();
 
    private SingletonSerializeAndDesrialize() {
 
    }
 
    public static SingletonSerializeAndDesrialize getInstance() {
        return singletonInstance;
    }
 
    public int getX() {
        return x;
    }
 
    public void setX(int x) {
        this.x = x;
    }
 
}

Сериализуйте наш объект, затем сделайте некоторые изменения в состоянии и затем сериализовайте его.

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.kb.singleton;
 
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
 
public class SerializeAndDeserializeTest {
 
    static SingletonSerializeAndDesrialize instanceOne = SingletonSerializeAndDesrialize.getInstance();
 
    public static void main(String[] args) {
        try {
            // Serialize to a file
             
            ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                    "filename.ser"));
            out.writeObject(instanceOne);
            out.close();
 
            instanceOne.setX(200);
 
            // Serialize to a file
            ObjectInput in = new ObjectInputStream(new FileInputStream(
                    "filename.ser"));
            SingletonSerializeAndDesrialize instanceTwo = (SingletonSerializeAndDesrialize) in.readObject();
            in.close();
 
            System.out.println(instanceOne.getX());
            System.out.println(instanceTwo.getX());
 
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }}

Выход:

200

100

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

Как преодолеть эту проблему? Значит, как предотвратить создание нового экземпляра при сериализации?

Решение очень простое — реализуйте метод ниже в вашем синглтон-классе:

1
2
Access_modifier  Object readResolve() throws ObjectStreamException{
}

Пример:

1
2
3
Public Object readResolve() throws ObjectStreamException{
return modifiedInstance;
}

Примените это к вышеуказанному одноэлементному классу, затем завершите одноэлементный класс, как показано ниже:

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
package com.kb.singleton;
 
import java.io.ObjectStreamException;
import java.io.Serializable;
 
public class SingletonSerializeAndDesrialize implements Serializable {
     
    private int x=100;
     
    private static volatile SingletonSerializeAndDesrialize singletonInstance = new SingletonSerializeAndDesrialize();
 
    private SingletonSerializeAndDesrialize() {
     System.out.println("inside constructor");
    }
 
    public static SingletonSerializeAndDesrialize getInstance() {
        return singletonInstance;
    }
 
    public int getX() {
        return x;
    }
 
    public void setX(int x) {
        this.x = x;
    }
     
    public Object readResolve() throws ObjectStreamException{
        return singletonInstance;
        }
 
 
}

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

Выход:

200

200

Это связано с тем, что во время сериализации он вызывает метод readResolve (), и там мы возвращаем существующий экземпляр, который предотвращает создание нового экземпляра и обеспечивает одноэлементный объект.

  • Осторожно с серийным идентификатором версии

Всякий раз, когда структура класса изменяется после того, как мы сериализовали и перед тем, как мы ее сериализовали. Затем во время процесса сериализации он находит несовместимый класс и, следовательно, выдает исключение: java.io.InvalidClassException: SingletonClass; несовместимый локальный класс: stream classdesc serialVersionUID = 5026910492258526905, локальный класс serialVersionUID = 3597984220566440782

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

1
private static final long serialVersionUID = 1L;

Итак, наконец, охватывая весь вышеописанный сценарий, наилучшее возможное решение синглтон-класса, как показано ниже, и я рекомендую использовать этот всегда:

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.kb.singleton;
 
import java.io.Serializable;
 
 
public class FinalSingleton implements Serializable{
     private static final long serialVersionUID = 1L;
     
    private FinalSingleton() {
         
    }
     
    private static class LazyLoadFinalSingleton{
        private static final FinalSingleton  SINGLETONINSTANCE = new FinalSingleton();
    }
     
    public static FinalSingleton getInstance(){
        return LazyLoadFinalSingleton.SINGLETONINSTANCE;
    }
     
    private Object readResolve() {
        return getInstance();
    }
 
 
}
Ссылка: Java Singleton Design Pattern от нашего партнера JCG Karibasappa GC на Java Guide Простой способ изучения блога Java .