单例模式 - 只有一个实例

只生成一个实例的模式,咱们称之为 单例模式。javascript

原文地址:单例模式 - 只有一个实例
博客地址:blog.720ui.com/java

程序在运行的时候,一般会有不少的实例。例如,咱们建立 100 个字符串的时候,会生成 100 个 String 类的实例。git

可是,有的时候,咱们只想要类的实例只存在一个。例如,「你猜我画」中的画板,在一个房间中的用户须要共用一个画板实例,而不是每一个用户都分配一个画板的实例。github

此外,对于数据库链接、线程池、配置文件解析加载等一些很是耗时,占用系统资源的操做,而且还存在频繁建立和销毁对象,若是每次都建立一个实例,这个系统开销是很是恐怖的,因此,咱们能够始终使用一个公共的实例,以节约系统开销。数据库

像这样确保只生成一个实例的模式,咱们称之为 单例模式设计模式

如何理解单例模式

单例模式的目的在于,一个类只有一个实例存在,即保证一个类在内存中的对象惟一性。 安全

如今,咱们来理解这个类图。微信

静态类成员变量

Singleton 类定义的静态的 instance 成员变量,并将其初始化为 Singleton 类的实例。这样,就能够保证单例类只有一个实例。多线程

私有的构造方法

Singleton 类的构造方法是私有的,这个设计的目的在于,防止类外部调用该构造方法。单例模式必需要确保在任何状况下,都只能生成一个实例。为了达到这个目的,必须设置构造方法为私有的。换句话说,Singleton 类必须本身建立本身的惟一实例。并发

全局访问方法

构造方法是私有的,那么,咱们须要提供一个访问 Singleton 类实例的全局访问方法。

简要定义

保证一个类只有一个实例,并提供一个访问它的全局访问方法。

单例模式的实现方式

饿汉式

顾名思义,类一加载对象就建立单例对象。

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return instance;
    }
}复制代码

值得注意的是,在定义静态变量的时候实例化 Singleton 类,所以在类加载的时候就能够建立了单例对象。

此时,咱们调用两次 Singleton 类的 getInstance() 方法来获取 Singleton 的实例。咱们发现 s1 和 s2 是同一个对象。

public class SingletonTest {

    @Test
    public void getInstance(){
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        System.out.println("实例对象1:" + s1.hashCode());
        System.out.println("实例对象2:" + s2.hashCode());
        if (s1 ==  s2) {
            System.out.println("实例相等");
        } else {
            System.out.println("实例不等");
        }
    }
}复制代码

懒汉式

懒汉式,即延迟加载。单例在第一次调用 getInstance() 方法时才实例化,在类加载时并不自动实例化,在须要的时候再进行加载实例。

public class Singleton2 {

    private Singleton2(){}

    private static Singleton2 instance = null;

    public static Singleton2 getInstance(){
        if(instance == null){
            instance = new Singleton2();
        }
        return instance;
    }
}复制代码

懒汉式的线程安全

在多线程中,若是使用懒汉式的方式建立单例对象,那就可能会出现建立多个实例的状况。

为了不多个线程同时调用 getInstance() 方法,咱们可使用关键字 synchronized 进行线程锁,以处理多个线程同时访问的问题。每一个类实例对应一个线程锁, synchronized 修饰的方法必须得到调用该方法的类实例的锁方能执行, 不然所属线程阻塞。方法一旦执行, 就独占该锁,直到从该方法返回时才将锁释放。此后被阻塞的线程方能得到该锁, 从新进入可执行状态。

public class Singleton3 {

    private Singleton3(){}

    private static Singleton3 instance = null;

    public static synchronized Singleton3 getInstance(){
        if(instance == null){
            instance = new Singleton3();
        }
        return instance;
    }
}复制代码

上面的案例,在多线程中很好的工做并且是线程安全的,可是每次调用 getInstance() 方法都须要进行线程锁定判断,在多线程高并发访问环境中,将会致使系统性能降低。事实上,不只效率很低,99%状况下不须要线程锁定判断。

这个时候,咱们能够经过双重校验锁的方式进行处理。换句话说,利用双重校验锁,第一次检查是否实例已经建立,若是还没建立,再进行同步的方式建立单例对象。

public class Singleton4 {

    private Singleton4(){}

    private static Singleton4 instance = null;

    public static Singleton4 getInstance(){
        if(instance == null){
            synchronized(Singleton4.class){
                if(instance == null){
                    instance = new Singleton4();
                }
            }    
        }
        return instance;
    }
}复制代码

枚举

枚举的特色是,构造方法是 private 修饰的,而且成员对象实例都是预约义的,所以咱们经过枚举来实现单例模式很是的便捷。

public enum SingletonEnum {
    INSTANCE;
    private SingletonEnum(){}
}复制代码

静态内部类

类加载的时候并不会实例化 Singleton5,而是在第一次调用 getInstance() 加载内部类 SigletonHolder,此时才进行初始化 instance 成员变量,确保内存中的对象惟一性。

public class Singleton5 {
    private Singleton5() {}

    private static class SigletonHolder {
        private final static Singleton5 instance = new Singleton5();
    }

    public static Singleton5 getInstance() {
        return SigletonHolder.instance;
    }
}复制代码

思惟发散

如何改形成单例类

假设,咱们如今有一个计数类 Counter 用来统计累加次数,每次调用 plus() 方法会进行累加。

public class Counter {

    private long count = 0;

    public long plus(){
        return ++count;
    }
}复制代码

这个案例的实现方式会生成多个实例,那么咱们如何使用单例模式确保只生成一个实例对象呢?

实际上,拆解成3个步骤就能够实现个人需求:静态类成员变量、私有的构造方法、全局访问方法。

public class Counter {

    private long count = 0;

    private static Counter counter = new Counter();

    private Counter(){}

    public static Counter getInstance(){
        return counter;
    }

    public synchronized long plus(){
        return ++count;
    }
}复制代码

多例场景

基于单例模式,咱们还能够进行扩展改造,获取指定个数的对象实例,节省系统资源,并解决单例对象共享过多有性能损耗的问题。

咱们来作个练习,我如今有一个需求,但愿实现最多只能生成 2 个 Resource 类的实例,能够经过 getInstance() 方法进行访问。

public class Resource {

    private int id = 0;

    private static Resource[] resource = new Resource[]{
        new Resource(1),
        new Resource(2)
    };

    private Resource(int id){
        this.id = id;
    }

    public static Resource getInstance(int id){
        return resource[id];
    }
}复制代码

单例模式 vs 静态方法

若是认为单例模式是非静态方法。而静态方法和非静态方法,最大的区别在因而否常驻内存,其实是不对的。它们都是在第一次加载后就常驻内存,因此方法自己在内存里,没有什么区别,因此也就不存在静态方法常驻内存,非静态方法只有使用的时候才分配内存的结论。

所以,咱们要从场景的层面来剖析这个问题。若是一个方法和他所在类的实例对象无关,仅仅提供全局访问的方法,这种状况考虑使用静态类,例如 java.lang.Math。而使用单例模式更加符合面向对象思想,能够经过继承和多态扩展基类。此外,上面的案子中,单例模式还能够进行延伸,对实例的建立有更自由的控制。

单例模式与数据库链接

数据库链接并非单例的,若是一个系统中只有一个数据库链接实例,那么所有数据访问都使用这个链接实例,那么这个设计确定致使性能缺陷。事实上,咱们经过单例模式确保数据库链接池只有一个实例存在,经过这个惟一的链接池实例分配 connection 对象。

总结

单例模式的目的在于,一个类只有一个实例存在,即保证一个类在内存中的对象惟一性。

若是采用饿汉式,在类被加载时就实例化,所以无须考虑多线程安全问题,而且对象一开始就得以建立,性能方面要优于懒汉式。

若是采用懒汉式,采用延迟加载,在第一次调用 getInstance() 方法时才实例化。好处在于无须一直占用系统资源,在须要的时候再进行加载实例。可是,要特别注意多线程安全问题,咱们须要考虑使用双重校验锁的方案进行优化。

实际上,咱们应该采用饿汉式仍是采用懒汉式,取决于咱们但愿空间换取时间,仍是时间换取空间的抉择问题。

此外,枚举和静态内部类也是很是不错的实现方式。

参考文章

(书)「图解设计模式」(结城浩)

源代码

相关示例完整代码: design-pattern-action

(完)

更多精彩文章,尽在「服务端思惟」微信公众号!

相关文章
相关标签/搜索