设计模式之单例模式

创建型模式之单例模式

目的

确保一个类只有一个实例,并为其提供一个全局的访问点。

解释

情景示例

政府就是一个很好的单例例子。一个国家只有一个官方政府,不管组成政府的每个人的身份是什么,"某政府"这一称谓总是鉴别哪些掌权者的全局
访问点

通俗来说

对于一个特定的类,确保只会创建一个对象

维基百科

在软件工程中,单例模式是一种软件设计模式,它将类的实例化限制为一个对象。当系统中只需要一个对象来协调各种操作时,这种模式非常有用。

动机

在软件系统中,经常有这样的一些类,必须保证它们在系统中存在一个实例,才能确保它们的逻辑正确性、以及不错的性能。
如何绕过常规的构造函数,提供一种机制来保证一个类只创建一个实例?就引出来了我们今天的主题单例模式。
从类使用者来看,单例模式的类应该是由类设计者的责任,而不是类使用者的责任。
实现单例模式的方式有很多种方案,我们接下来一一介绍

单例模式实现

单例模式-饿汉式

在饿汉式单例模式中,单例类的实例是在类加载时创建的,这是创建单例类最简单的方法,但它有一个缺点,即使客户端应用程序可能不使用它,也会创建实例。浪费内存

下面代码是饿汉式的实现

public class EagerInitializationSingleton {

    private static final EagerInitializationSingleton inst = new EagerInitializationSingleton();

    private EagerInitializationSingleton(){}

    public static EagerInitializationSingleton getInstance() {
        return inst;
    }

}

如果您的单例类没有使用大量资源,则可以使用这种方法。但在大多数情况下,单例类是为文件系统、数据库连接等资源创建的,
我们应该避免实例化,除非客户端调用getInstance方法。此外,此方法不提供任何异常处理选项。

单例模式-静态代码块

静态块初始化的实现类似单例模式饿汉式,除了类的实例是在静态块中创建的,它提供了异常处理的选项。

public class StaticBlockSingleton {

    private static StaticBlockSingleton inst;

    private StaticBlockSingleton(){}

    static {
        try {

            inst = new StaticBlockSingleton();
        }catch (Exception e) {
            throw new RuntimeException("create StaticBlockSingleton inst exception");
        }
    }

    public static StaticBlockSingleton getInstance() {
        return inst;
    }

}

单例模式饿汉式初始化和静态块初始化都是在使用实例之前创建实例,这不是最佳实践。因此,在后面的小节中,
我们将学习如何创建支持延迟初始化的Singleton类。

单例模式-懒汉式

懒汉式单例模式指的是延迟初始化方法在全局访问方法中创建实例。下面是用于创建的示例代码

public class LazyInitializationSingleton {

    private static LazyInitializationSingleton inst;

    private LazyInitializationSingleton(){}

    public static LazyInitializationSingleton getInstance() {
        if (inst == null) {
            inst = new LazyInitializationSingleton();
        }
        return inst;
    }

}

单例模式-缓存实现

使用缓存思想,可以变相实现单例模式,也算是一个模拟实现吧,每次都是先从缓存中获取值。只要创建一次对象实例后,就设置缓存的值,那么
下次就不用再创建了。示例代码如下:

public class CacheSingleton {

    // 定义一个默认的key,用来标识在缓存中的存放
    private final static String DEFAULT_KEY = "default";

    // 缓存实例的容器
    private static Map<String,CacheSingleton> map = new HashMap<>();

    private CacheSingleton(){}

    public static CacheSingleton getInstance() {
        // 先从缓存中获取
        CacheSingleton inst = map.get(DEFAULT_KEY);
        // 如果没有就创建一个,然后设置回缓存中
        if (inst == null ) {
            inst = new CacheSingleton();
            map.put(DEFAULT_KEY,inst);
        }
        return inst;
    }
}

简单测试程序,验证上述代码是否是线程安全

public class CacheSingletonTest {

    public static void main(String[] args) {
        for (int i=0;i<10000;i++) {
            new Thread(()->{
                CacheSingleton singleton = CacheSingleton.getInstance();
                System.out.println(Thread.currentThread().getName()+"::::::"+singleton);
            }).start();
        }
    }

}

运行日志

Thread-8::::::com.airycode.creational.singleton.CacheSingleton@219a0906
Thread-5::::::com.airycode.creational.singleton.CacheSingleton@219a0906
Thread-0::::::com.airycode.creational.singleton.CacheSingleton@37cb6108
Thread-10::::::com.airycode.creational.singleton.CacheSingleton@219a0906
Thread-13::::::com.airycode.creational.singleton.CacheSingleton@219a0906
Thread-14::::::com.airycode.creational.singleton.CacheSingleton@219a0906
Thread-1::::::com.airycode.creational.singleton.CacheSingleton@4025fef1

上面的实现在单线程环境下工作得很好,但是当涉及到多线程系统时,如果多个线程同时在if循环中,它可能会导致问题。
它将破坏单例模式,两个线程将获得单例类的不同实例。接下来,我们将看到创建线程安全的单例类的不同方法。

单例模式-线程安全

创建线程安全的单例类的更简单的方法是使全局访问方法同步,这样一次只有一个线程可以执行该方法。这种方法的一般实现类似于下面代码。

public class ThreadSafeSingleton {

    // 使用volatile,保证变量的可见性,屏蔽指令重排
    private volatile static ThreadSafeSingleton inst;

    private ThreadSafeSingleton(){}

    public static synchronized ThreadSafeSingleton getInstance() {
        if (inst == null) {
            inst = new ThreadSafeSingleton();
        }
        return inst;
    }
}

上面的实现工作得很好,并且提供了线程安全性,但是由于与synchronized方法相关的成本,它降低了性能,
尽管我们只需要对可能创建单独实例的前几个线程使用它(阅读:Java Synchronization)。
为了避免每次都出现这种额外的开销,使用了双重检查锁定原则。在这种方法中,同步块在if条件中使用,并进行额外的检查,以确保只创建了一个单例类实例。
注意:上述代码中的关键字volatile,由于JVM可能会对上面的步骤进行指令重排,因此,我们需要使用volatile,保证变量的可见性,屏蔽指令重排

public class ThreadSafeSingleton {

    // 使用volatile,保证变量的可见性,屏蔽指令重排
    private volatile static ThreadSafeSingleton inst;

    private ThreadSafeSingleton(){}

    public static synchronized ThreadSafeSingleton getInstance() {
        if (inst == null) {
            inst = new ThreadSafeSingleton();
        }
        return inst;
    }

    public static synchronized ThreadSafeSingleton getInstanceUsingDoubleLocking() {
            // 第一次判断,如果inst不为null,不进入抢锁,直接返回实例
            if (inst == null) {
                synchronized (ThreadSafeSingleton.class) {
                    // 第二次判断,抢到锁之后再进行判断是否为null
                    if (inst == null) {
                        // 创建对象的代码,在JVM中被分为三步
                        // 1.分配内存
                        // 2.初始化对象
                        // 3.将instance指向分配好的内存空间
                        // JVM可能会对上面的步骤进行指令重排,因此,我们需要使用volatile,保证变量的可见性,屏蔽指令重排
                        inst = new ThreadSafeSingleton();
                    }
                }
            }
            return inst;
        }
}

让我们用一个示例类来看看。是否还有指令重排的问题

public class ThreadSafeSingletonTest {

    public static void main(String[] args) {
        for (int i=0;i<100;i++) {
            new Thread(()->{
                ThreadSafeSingleton safeSingleton = ThreadSafeSingleton.getInstance();
                System.out.println(Thread.currentThread().getName()+"::::::"+safeSingleton);
            }).start();
        }
    }

}

运行日志:

Thread-9::::::com.airycode.creational.singleton.ThreadSafeSingleton@4025fef1
Thread-11::::::com.airycode.creational.singleton.ThreadSafeSingleton@4025fef1
Thread-10::::::com.airycode.creational.singleton.ThreadSafeSingleton@4025fef1
Thread-8::::::com.airycode.creational.singleton.ThreadSafeSingleton@4025fef1
Thread-3::::::com.airycode.creational.singleton.ThreadSafeSingleton@4025fef1
Thread-6::::::com.airycode.creational.singleton.ThreadSafeSingleton@4025fef1
Thread-7::::::com.airycode.creational.singleton.ThreadSafeSingleton@4025fef1
Thread-16::::::com.airycode.creational.singleton.ThreadSafeSingleton@4025fef1
Thread-0::::::com.airycode.creational.singleton.ThreadSafeSingleton@4025fef1

通过日志可知:实例的hashcode都是一个,指令重排的问题已解决。

单例模式-内部托管模式(比尔普格)

在Java 5之前,Java内存模型有很多问题,在某些情况下,当太多线程试图同时获得Singleton类的实例时,
上面的方法会失败。因此比尔普格提出了一种不同的方法,使用内部静态内部类来创建Singleton类。比尔普格的单例实现如下:

public class BillPughSingleton {

    private BillPughSingleton(){}

    private static class SingletonHelper{
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance(){
        return SingletonHelper.INSTANCE;
    }
    
}

注意私有内部静态类,它包含单例类的实例。当singleton类被加载时,SingletonHelper类不会被加载到内存中,
只有当有人调用getInstance方法时,这个类才会被加载并创建singleton类实例。
这是Singleton类最广泛使用的方法,因为它不需要同步。我在我的许多项目中使用这种方法,它也很容易理解和实现。

使用反射破坏单例模式

反射可以用来破坏上述所有的单例实现方法。让我们用一个示例类来看看。

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        EagerInitializationSingleton instanceOne = EagerInitializationSingleton.getInstance();
        EagerInitializationSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializationSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                //Below code will destroy the singleton pattern
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializationSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

运行日志:

460141958
1163157884

当您运行上面的测试类时,您会注意到两个实例的hashCode不相同,这会破坏单例模式。
反射功能非常强大,在很多框架中都有使用,比如Spring和Hibernate。

单例模式-枚举方式

为了防止反射破坏单例Joshua Bloch建议使用枚举实现Java的单例设计模式,确保任何枚举值在Java程序中只实例化一次。
因为Java枚举值是全局可访问的,所以单例也是。
缺点是枚举类型有些不灵活;例如,它不允许延迟初始化。示意代码如下:

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething(){
        System.out.println("do some thing");
    }
}

序列化与单例模式爱恨情仇

有时在分布式系统中,我们需要在Singleton类中实现Serializable接口,以便将其状态存储在文件系统中
并在以后的时间点恢复。代码如下:

public class SerializedSingleton implements Serializable {

    private SerializedSingleton(){}

    private static class SingletonHelper{
        private static final SerializedSingleton instance = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance(){
        return SingletonHelper.instance;
    }

}

上述序列化的单例类的问题是,每当我们反序列化它时,它将创建一个新的类实例。让我们用一个简单的测试程序来看看。

public class SingletonSerializedTest {

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();

        //deserailize from file to object
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();

        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());

    }

}

运行日志:

instanceOne hashCode=2133927002
instanceTwo hashCode=1836019240

因此它破坏了单例模式,为了解决这种情况,我们所需要做的就是提供readResolve()方法的实现。代码如下:

public class SerializedSingleton implements Serializable {

    private SerializedSingleton(){}

    private static class SingletonHelper{
        private static final SerializedSingleton instance = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance(){
        return SingletonHelper.instance;
    }

    // 解决序列化破坏单例
    protected Object readResolve() {
        return getInstance();
    }

}

在实现该方法后一会发现,上述两个单例hashcode是一样的。
运行日志:

instanceOne hashCode=2133927002
instanceTwo hashCode=2133927002

适用性

当满足以下情况时,使用单例模式:
确保一个类只有一个实例,并且客户端能够通过一个众所周知的访问点访问该实例。
唯一的实例能够被子类扩展, 同时客户端不需要修改代码就能使用扩展后的实例。
一些典型的单例模式用例包括:
logging类
管理与数据库的链接
文件管理器(File manager)

源码应用

java.lang.Runtime#getRuntime()
java.awt.Desktop#getDesktop()
java.lang.System#getSecurityManager()

影响

通过控制实例的创建和生命周期,违反了单一职责原则(SRP)。
鼓励使用全局共享实例,组织了对象及其使用的资源被释放。
代码变得耦合,给客户端的测试带来难度。
单例模式的设计可能会使得子类化(继承)单例变得几乎不可能

思考扩展

单例模式的本质:控制实例数目

单例模式是为了控制运行期间,某些类的实例数目只有一个。可能有人就会想,能不能控制实例的数目是2个,3个或者多个。目的都是一样的节约
资源。如下:使用Map缓存3个实例数目,上面我们已经讲过缓存方式实现单例模式,我们知道map中有多个实例,那么在客户端调用的时候,到底
返回哪一个实例,也就是实例调度问题,我们只是展示设计模式,不考虑实例调度的算法。
参考缓存方式变形实现多例模式

public class MultiInitializationMode {

    // 缓存前缀
    private final static String DEFAULT_KEY = "DEFAULT";

    // 缓存实例容器
    private static Map<String,MultiInitializationMode> map = new HashMap<>();

    // 用于记录当前正在使用第几个实例,到了控制的最大数目,就返回从1开始
    private static int num = 1;

    // 控制实例的最大数目
    private final static int NUM_MAX = 3;

    private MultiInitializationMode(){}

    public static MultiInitializationMode getInstance() {
        String key = DEFAULT_KEY+num;
        // 缓存的体现,通过控制缓存的数据多少来控制实例数目
        MultiInitializationMode multiInitializationMode = map.get(key);
        if (multiInitializationMode == null) {
            multiInitializationMode = new MultiInitializationMode();
            map.put(key,multiInitializationMode);
        }
        // 把当前实例的序号加1
        num++;
        if (num > NUM_MAX) {
            // 如果实例的序号已经达到最大数目了,那就重复从1开始获取
            num = 1;
        }
        return multiInitializationMode;
    }
}

测试程序如下:

public class MultiInitializationModeTest {

    public static void main(String[] args) {
        MultiInitializationMode t1 = MultiInitializationMode.getInstance();
        MultiInitializationMode t2 = MultiInitializationMode.getInstance();
        MultiInitializationMode t3 = MultiInitializationMode.getInstance();
        MultiInitializationMode t4 = MultiInitializationMode.getInstance();
        MultiInitializationMode t5 = MultiInitializationMode.getInstance();
        MultiInitializationMode t6 = MultiInitializationMode.getInstance();
        System.out.println("t1="+t1);
        System.out.println("t2="+t2);
        System.out.println("t3="+t3);
        System.out.println("t4="+t4);
        System.out.println("t5="+t5);
        System.out.println("t6="+t6);
    }

}

运行日志:

t1=com.airycode.creational.singleton.MultiInitializationMode@1b6d3586
t2=com.airycode.creational.singleton.MultiInitializationMode@4554617c
t3=com.airycode.creational.singleton.MultiInitializationMode@74a14482
t4=com.airycode.creational.singleton.MultiInitializationMode@1b6d3586
t5=com.airycode.creational.singleton.MultiInitializationMode@4554617c
t6=com.airycode.creational.singleton.MultiInitializationMode@74a14482
  • 24
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值