[Singleton Pattern] Usage Comparison of Java Design Patterns

https://blog.csdn.net/luofen521/article/details/51788230

1. Definition

Make sure that there is only one instance of a class, that it can instantiate itself and make that instance available to the entire system.

2. Application scenarios

  1. When multiple objects are generated, it will consume too much resources, such as IO and data operations
  2. There should only be one and only one object of a certain type, such as Application in Android.

3. Consider the situation

  1. Instances are not unique due to multithreading.
  2. The deserialization process produces a new instance.

4. Implementation

4.1 Ordinary singleton pattern

/**
 * 普通模式
 * @author josan_tang
 */
public class SimpleSingleton {
    //1.static单例变量
    private static SimpleSingleton instance;

    //2.私有的构造方法
    private SimpleSingleton() {

    }

    //3.静态方法为调用者提供单例对象
    public static SimpleSingleton getInstance() {
        if (instance == null) {
            instance = new SimpleSingleton();
        }
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

In the case of multi-threading and high concurrency, there will be obvious problems in writing this way. When thread A calls the getInstance method and executes line 16, it detects that the instance is null, so it executes line 17 to instantiate the instance. When line 17 is not executed At the end, thread B calls the getInstance method again. At this time, it detects that the instance is still empty, so thread B also executes line 17 to create a new instance. At this time, the instance obtained by thread A and thread B is not the same, which violates the definition of singleton.

4.2 Hungry singleton pattern

/**
 * 饿汉单例模式
 * @author josan_tang
 */
public class EHanSingleton {
    //static final单例对象,类加载的时候就初始化
    private static final EHanSingleton instance = new EHanSingleton();

    //私有构造方法,使得外界不能直接new
    private EHanSingleton() {
    }

    //公有静态方法,对外提供获取单例接口
    public static EHanSingleton getInstance() {
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

Hungry singleton mode solves the problem of multi-threaded concurrency, because instance is instantiated when this class is loaded. When the getInstatnce method is called, the object is always initialized when the class is loaded (except in the case of deserialization). But this also brings another problem. If a large number of classes adopt the hungry singleton mode, then in the class loading stage, many objects that have not been used for the time being will be initialized, which will definitely waste memory and affect performance. , we still have to tend to the 4.1 approach, and only initialize the instance when the getInstance method is called for the first time. Please continue to see 4.3 usage.

4.3 The Lazy Singleton Pattern

import java.io.Serializable;

/**
 * 懒汉模式
 * @author josan_tang
 */
public class LanHanSingleton {
    private static LanHanSingleton instance;

    private LanHanSingleton() {

    }

    /**
     * 增加synchronized关键字,该方法为同步方法,保证多线程单例对象唯一
     */
    public static synchronized LanHanSingleton getInstance() {
        if (instance == null) {
            instance = new LanHanSingleton();
        }
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

The only difference from 4.1 is that the synchronized keyword is added before the getInstance method, making the getInstance method a synchronized method, which ensures that when getInstance is called for the first time, that is, when the instance is instantiated, other calls will not enter the method. , which ensures the uniqueness of singleton objects in multithreading.

优点:单例对象在第一次调用才被实例化,有效节省内存,并保证了线程安全。

缺点:同步是针对方法的,以后每次调用getInstance时(就算intance已经被实例化了),也会进行同步,造成了不必要的同步开销。不推荐使用这种方式。

4.4 Double CheckLock(DCL)单例模式

/**
 * Double CheckLock(DCL)模式
 * @author josan_tang
 *
 */
public class DCLSingleton {
    //增加volatile关键字,确保实例化instance时,编译成汇编指令的执行顺序
    private volatile static DCLSingleton instance;

    private DCLSingleton() {

    }

    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                //当第一次调用getInstance方法时,即instance为空时,同步操作,保证多线程实例唯一
                //当以后调用getInstance方法时,即instance不为空时,不进入同步代码块,减少了不必要的同步开销
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

DCL失效:

在JDK1.5之前,可能会有DCL实现的问题,上述代码中的20行,在Java里虽然是一句代码,但它并不是一个真正的原子操作。

instance = new DCLSingleton();
  • 1

它编译成最终的汇编指令,会有下面3个阶段:

  1. 给DCLSingleton实例分配内存
  2. 调用DCLSingleton的构造函数,初始化成员变量。
  3. 将instance指向分配的内存空间(这个操作以后,instance才不为null)

在jdk1.5之前,上述的2、3步骤不能保证顺序,也就是说有可能是1-2-3,也有可能是1-3-2。如果是1-3-2,当线程A执行完步骤3(instance已经不为null),但是还没执行完2,线程B又调用了getInstance方法,这时候线程B所得到的就是线程A没有执行步骤2(没有执行完构造函数)的instance,线程B在使用这样的instance时,就有可能会出错。这就是DCL失效。

在jdk1.5之后,可以使用volatile关键字,保证汇编指令的执行顺序,虽然会影响性能,但是和程序的正确性比起来,可以忽悠不计。

Java内存模型

优点:第一次执行getInstance时instance才被实例化,节省内存;多线程情况下,基本安全;并且在instance实例化以后,再次调用getInstance时,不会有同步消耗。

缺点:jdk1.5以下,有可能DCL失效;Java内存模型影响导致失效;jdk1.5以后,使用volatile关键字,虽然能解决DCL失效问题,但是会影响部分性能。

4.5 静态内部类单例模式

/**
 * 静态内部类实现单例模式
 * @author josan_tang
 *
 */
public class StaticClassSingleton {
    //私有的构造方法,防止new
    private StaticClassSingleton() {

    }

    public static StaticClassSingleton getInstance() {
        return StaticClassSingletonHolder.instance;
    }

    /**
     * 静态内部类
     */
    private static class StaticClassSingletonHolder {
        //第一次加载内部类的时候,实例化单例对象
        private static final StaticClassSingleton instance = new StaticClassSingleton();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

第一次加载StaticClassSingleton类时,并不会实例化instance,只有第一次调用getInstance方法时,Java虚拟机才会去加载StaticClassSingletonHolder类,继而实例化instance,这样延时实例化instance,节省了内存,并且也是线程安全的。这是推荐使用的一种单例模式。

4.6 枚举单例模式

/**
 * 枚举单例模式
 * @author josan_tang
 *
 */
public enum EnumSingleton {
    //枚举实例的创建是线程安全的,任何情况下都是单例(包括反序列化)
    INSTANCE;

    public void doSomething(){

    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

枚举不仅有字段还能有自己的方法,并且枚举实例创建是线程安全的,就算反序列化时,也不会创建新的实例。除了枚举模式以外,其他实现方式,在反序列化时都会创建新的对象。

为了防止对象在反序列化时创建新的对象,需要加上如下方法:

    private Object readResole() throws ObjectStreamException {
        return instance;
    }
  • 1
  • 2
  • 3

这是一个钩子函数,在反序列化创建对象时会调用它,我们直接返回instance就是说,不要按照默认那样去创建新的对象,而是直接将instance返回。

4.7 容器单例模式

import java.util.HashMap;
import java.util.Map;

/**
 * 容器单例模式
 * @author josan_tang
 */
public class ContainerSingleton {
    private static Map<String, Object> singletonMap = new HashMap<String, Object>();

    //单例对象加入到集合,key要保证唯一性
    public static void putSingleton(String key, Object singleton){
        if (key != null && !"".equals(key) && singleton != null) {
            if (!singletonMap.containsKey(key)) {   //这样防止集合里有一个类的两个不同对象
                singletonMap.put(key, singleton);   
            }
        }
    }

    //根据key从集合中得到单例对象
    public static Object getSingleton(String key) {
        return singletonMap.get(key);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

在程序初始化的时候,讲多种单例类型对象加入到一个单例集合里,统一管理。在使用时,通过key从集合中获取单例对象。这种方式多见于系统中的单例,像安卓中的系统级别服务就是采用集合形式的单例模式,比如常用的LayoutInfalter,我们一般在Fragment中的getView方法中使用如下代码:

View view = LayoutInflater.from(context).inflate(R.layout.xxx, null);
  • 1

其实LayoutInflater.from(context)就是得到LayoutInflater实例,看下面的Android源码:

    /**
     * Obtains the LayoutInflater from the given context.
     */
    public static LayoutInflater from(Context context) {
        //通过key,得到LayoutInflater实例
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

总结

不管是那种形式实现单例模式,其核心思想都是将构造方法私有化,并且通过静态方法获取一个唯一的实例。在获取过程中需要保证线程安全、防止反序列化导致重新生成实例对象。选择哪种实现方式看情况而定,比如是否高并发、JDK版本、单例对象的资源消耗等。

名称 优点 缺点 备注
简单模式 实现简单 线程不安全  
饿汉模式 线程安全 内存消耗太大  
懒汉模式 线程安全 同步方法消耗比较大  
DCL模式 线程安全,节省内存 jdk版本受限、高并发会导致DCL失效 推荐使用
静态内部类模式 线程安全、节省内存 实现比较麻烦 推荐使用
枚举模式 线程安全、支持反序列化 个人感觉比较怪异  
集合模式 统一管理、节省资源 线程不安全

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326023818&siteId=291194637