文章目录

  1. 1. 如何理解单例模式
    1. 1.1. 静态类成员变量
    2. 1.2. 私有的构造方法
    3. 1.3. 全局访问方法
  2. 2. 简要定义
  3. 3. 单例模式的实现方式
    1. 3.1. 饿汉式
    2. 3.2. 懒汉式
    3. 3.3. 懒汉式的线程安全
    4. 3.4. 枚举
    5. 3.5. 静态内部类
  4. 4. 思维发散
    1. 4.1. 如何改造成单例类
    2. 4.2. 多例场景
    3. 4.3. 单例模式 vs 静态方法
    4. 4.4. 单例模式与数据库连接
    5. 4.5. volatile 修饰
  5. 5. 总结
  6. 6. 参考文章
  7. 7. 源代码

程序在运行的时候,通常会有很多的实例。例如,我们创建 100 个字符串的时候,会生成 100 个 String 类的实例。

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

此外,对于数据库连接、线程池、配置文件解析加载等一些非常耗时,占用系统资源的操作,并且还存在频繁创建和销毁对象,如果每次都创建一个实例,这个系统开销是非常恐怖的,所以,我们可以始终使用一个公共的实例,以节约系统开销。

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

如何理解单例模式

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

现在,我们来理解这个类图。

静态类成员变量

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 对象。

volatile 修饰

对象的创建并不是一个原子操作,在 new 对象的时候其实是有 3 步:分配没存,初始化和赋值。由于 java 是允许处理器进行乱序执行的,所以有可能是先赋值再初始化,这样懒汉模式就有异常了,解决方法是给这个静态对象加 volatile 字段来防止乱序执行。

总结

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

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

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

实际上,我们应该采用饿汉式还是采用懒汉式,取决于我们希望空间换取时间,还是时间换取空间的抉择问题。

此外,静态内部类也是非常不错的实现方式。

参考文章

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

源代码

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

(完)

微信公众号

文章目录

  1. 1. 如何理解单例模式
    1. 1.1. 静态类成员变量
    2. 1.2. 私有的构造方法
    3. 1.3. 全局访问方法
  2. 2. 简要定义
  3. 3. 单例模式的实现方式
    1. 3.1. 饿汉式
    2. 3.2. 懒汉式
    3. 3.3. 懒汉式的线程安全
    4. 3.4. 枚举
    5. 3.5. 静态内部类
  4. 4. 思维发散
    1. 4.1. 如何改造成单例类
    2. 4.2. 多例场景
    3. 4.3. 单例模式 vs 静态方法
    4. 4.4. 单例模式与数据库连接
    5. 4.5. volatile 修饰
  5. 5. 总结
  6. 6. 参考文章
  7. 7. 源代码