原创

设计模式详解(三)——单例模式


一、场景问题

设计单体应用中的序列生成器。

二、解决方案

考虑到序列生成器的使用频繁性,我们可以使用单例模式,即始终只存在一个序列生成器实例,每次都调用实例来获取最新的序列,不仅减少类的资源占用,同时也保证了序列的唯一性。

1、序列生成器类

public class UidGenerator {
    /**
     * 唯一序列
     */
    private AtomicInteger uniqueSequence = new AtomicInteger(0);

    /**
     * 单例
     */
    private static UidGenerator uidGenerator = new UidGenerator();

    //构造方法私有化,禁止被外部实例化
    private UidGenerator() {
    }

    /**
     * 获取实例
     * @author xianzilei
     **/
    public static UidGenerator getInstance() {
        return uidGenerator;
    }

    /**
     * 生成唯一序列
     * @author xianzilei
     **/
    public int getNextId() {
        return uniqueSequence.incrementAndGet();
    }
}

2、客户端代码

public static void main(String[] args) {
        //获取实例1
        UidGenerator uidGenerator1 = UidGenerator.getInstance();
        //获取实例2
        UidGenerator uidGenerator2 = UidGenerator.getInstance();
        //判断实例1与实例2是否是同一实例
        System.out.println(uidGenerator1 == uidGenerator2);

        //分别使用实例1和实例2获取序列
        System.out.println(uidGenerator1.getNextId());
        System.out.println(uidGenerator2.getNextId());
        System.out.println(uidGenerator1.getNextId());
        System.out.println(uidGenerator2.getNextId());
        System.out.println(uidGenerator2.getNextId());
        System.out.println(uidGenerator1.getNextId());
    }

输出

true
1
2
3
4
5
6

获取的实例始终是同一个实例,且序列生成始终递增无重复。

三、模式概述

相对于前面讲解的工厂模式,单例模式理解起来相当简单,即程序运行过程了始终保持一个实例。

1、模式定义

单例模式(Singleton Pattern):指一个类只有一个实例,且该类能自行创建这个实例的一种模式。

单例模式有三个特点

  • 只有一个实例对象
  • 该实例对象只能自行创建
  • 单例类对外提供一个访问该类的全局访问点

2、模式结构

  • 单例类

    可自行创建且只存在一个实例的类

  • 访问类

    单例的使用者

3、模式结构图

4、单例模式的八种实现方式

4.1、饿汉式(静态变量)

该方式即为场景中使用到的创建方式,饿汉式表示未使用到单例就直接创建对象,即创建实例在使用到之前就完成了。饿汉式没有线程安全问题,但是会耗费内存资源

public class Singleton1 {
    //1.构造函数私有化
    private Singleton1() {

    }

    //2.创建对象实例
    private static Singleton1 instance = new Singleton1();

    //3.提供一个公有的静态方法返回实例对象
    public static Singleton1 getInstance() {
        return instance;
    }
}

4.2、饿汉式(静态代码块)

同静态变量方法,只不过创建对象放到了静态代码块中,优点在于初始化单例时可以做些其他初始化操作

public class Singleton2 {
    //1.构造函数私有化
    private Singleton2() {
    }

    //2.创建对象实例,通过静态代码块实例化
    private static Singleton2 instance;

    //3.通过静态代码块实例化
    static {
        instance = new Singleton2();
        //其他初始化操作...
    }

    //4.提供一个公有的静态方法返回实例对象
    public static Singleton2 getInstance() {
        return instance;
    }
}

4.3、懒汉式(线程不安全)

相对于饿汉式,懒汉式是使用到单例时才会创建实例,后续使用到的都是同一个实例。懒汉式可以减少资源消耗和内存占用

public class Singleton3 {
    //1.构造函数私有化
    private Singleton3() {
    }

    //2.定义对象引用
    private static Singleton3 instance;

    //3.提供一个公有的静态方法返回实例对象
    public static Singleton3 getInstance() {
        //如果未实例化,创建实例
        if (instance == null) {
            instance = new Singleton3();
        }
        //返回实例
        return instance;
    }
}

注意:在多线程环境下,上面的获取实例方法没有任何同步限制,所以可能会带来线程安全问题,所以一般不使用。

4.4、懒汉式(线程安全,同步方法)

既然上面的方法可能存线程安全问题,我们可以使用synchronized关键字修饰获取实例的方法,使其成为同步方法,这样就可以保证线程安全问题

public class Singleton4 {
    //1.构造函数私有化
    private Singleton4() {
    }

    //2.定义对象引用
    private static Singleton4 instance;

    //3.提供一个公有的静态方法返回实例对象
    public static synchronized Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }
        return instance;
    }
}

4.5、懒汉式(线程安全,同步代码块)

有时候获取实例的方法中除了实例化单例,还会有其他额外的操作,给整个方法加上同步锁未免耗费过大,因此可以采用同步代码块的方式,减小锁的锁定范围,减少系统消耗

public class Singleton5 {
    //1.构造函数私有化
    private Singleton5() {
    }

    //2.定义对象引用
    private static Singleton5 instance;

    //3.提供一个公有的静态方法返回实例对象
    public static Singleton5 getInstance() {
        //同步代码块
        synchronized (Singleton5.class) {
            if (instance == null) {
                instance = new Singleton5();
            }
        }
        //其余操作...
        
        //返回实例
        return instance;
    }
}

4.6、懒汉式(双重检测DCL

为了追求极致的效率,上面的方法还是存在效率问题。每个线程都需要等待同步块执行结束才能执行,可能对象已经初始化结束了,但是同步锁未释放,导致其他线程白白多等待一会。因此我们可以使用双重检测机制

public class Singleton6 {
    //1.构造函数私有化
    private Singleton6() {
    }

    //2.定义对象引用(这里使用volatile修饰是避免CPU指令的重排序导致的对象未完全初始化结束的引用逸出)
    private static volatile Singleton6 instance;

    //3.提供一个公有的静态方法返回实例对象
    public static synchronized Singleton6 getInstance() {
        //第一次判断不加锁,提高执行效率,避免了不必要的同步
        if (instance == null) {
            synchronized (Singleton6.class) {
                if (instance == null) {
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }
}

这里需要注意:变量需要使用volatile修饰,避免CPU指令的重排序导致的对象未完全初始化结束的引用逸出

4.7、静态内部类式

上面的方法貌似写法一个比一个复杂,我们可以从类加载的机制来实现单例模式。

public class Singleton7 {
    //1.构造函数私有化
    private Singleton7() {
    }

    //2.创建静态私有内部类,定义单例属性实例(静态内部类在主类装载时不会被装载)
    private static class SingletonInstance {
        private static final Singleton7 INSTANCE = new Singleton7();
    }

    //3.提供一个公有的静态方法返回实例对象
    public static Singleton7 getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

这里是利用了类加载的两个特点:

  • 静态内部类在主类加载的时候不会被加载,即实现了懒汉模式,减少内存占用。
  • 类加载的过程是JVM层面保证了线程安全性,即同一个类只能加载一次

4.8、枚举式

看了那么多优秀的单例模式实现方式,为什么还有第八种?这是因此上面的方法在一种情况下都会重新创建实例,即反序列化,将一个单例对象写到磁盘上,则读回来,得到是一个新的实例。因此终极方式即为枚举类。

public enum Singleton8 {
    INSTANCE;
}

枚举类天生单例,线程安全,且为懒汉式。但是实际工作中很少使用,因为代码可读性较差,且枚举类大部分作为枚举常量使用。

5、优缺点

  • 优点
    • 提高了唯一的访问实例,可以保证访问资源及操作的一致性,即控制客户行为及访问数据。
    • 节约内存资源,提高性能。
  • 缺点
    • 单例模式没有层级关系,不利于在原单例类上的扩展
    • 违背单一职责原则。单例类既作为单例实例的类,也充当单例的创建者,同时也包含业务逻辑。
  • 系统中有多于一个的产品族,而每次只使用其中某一产品族。可以通过配置文件等方式来使得用户可以动态改变产品族,也可以很方便地增加新的产品族。
  • 产品等级结构稳定,设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构。

6、使用场景

  • 系统只需要某一个类的唯一实例,例如唯一序列生成器。
  • 耗费较大的资源类可以考虑单例模式减少系统损耗。
  • 控制单一访问节点或资源,可以考虑使用单例模式。

四、模式扩展

JDK中的单例模式体现

JDK中的Runtime类,每个JVM进场都对应一个Runtime实例,保存JVM的运行中的一些参数信息。因此JDK设计Runtime类为单例形式。源码如下:

public class Runtime {
    //饿汉式创建Runtime实例
    private static Runtime currentRuntime = new Runtime();

    //获取Runtime实例
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    //构造方法私有化
    private Runtime() {}
    
    //其余代码省略...
}
Java
设计模式
  • 作者:贤子磊
  • 发表时间:2021-05-17 12:32
  • 版权声明:自由转载-非商用-非衍生-保持署名
  • 评论

    您需要登录后才可以评论