23种设计模式之单例模式

23种设计模式之单例模式

参考资料

下文如有错漏之处,敬请指正

一、简介

定义

一个类只存在一个实例对象,并且自行实例化并向全局提供一个获取该实例的方法。

特点

  • 单例模式是一种创建型模式
  • 构造方法私有化,提供一个static方法用来获取实例

优点

  • 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

  • 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。

缺点

  • 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。
  • 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。

通用类图

在这里插入图片描述

使用场景

  • 任务管理器

  • 回收站

  • 数据库连接池

  • Spring的Bean

二、实现方式

  • 饿汉式 :在类加载时创建(线程安全)
  • 懒汉式:在调用时创建(非线程安全)
  • DCL(线程安全)
  • 静态内部类(线程安全)
  • 枚举(推荐使用,保证线程安全、不支持克隆、反射和反序列化操作)

饿汉式

不管是否使用该对象,JVM一启动就创建之。

package singleTon;

import java.lang.reflect.Constructor;

public class HungrySingleTon {

    private String slogan;

    private HungrySingleTon(String slogan) {
        this.slogan = slogan;
    }

    private final static HungrySingleTon HUNGRY_SINGLE_TON = new HungrySingleTon("饿汉一号准备就绪");

    public static HungrySingleTon getInstance() {
        return HUNGRY_SINGLE_TON;
    }

    public static void main(String[] args) throws Exception {
        HungrySingleTon hungrySingleTon1 = HungrySingleTon.getInstance();
        HungrySingleTon hungrySingleTon2 = HungrySingleTon.getInstance();
        System.out.println(hungrySingleTon1 == hungrySingleTon2);//true

        // 通过反射使用HungrySingleTon类的构造器创建实例
        Constructor<HungrySingleTon> constructor = HungrySingleTon.class.getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        HungrySingleTon reflectSingleTon = constructor.newInstance("反射饿汉一号准备就绪");
        System.out.println(hungrySingleTon1 == reflectSingleTon);//false
    }
}

饿汉式的缺点是浪费内存,如果定义了1000个饿汉式的单例模式,则 JVM启动时会立即为1000个饿汉式创建1000个实例对象。

上述饿汉式虽然保证了线程安全,但是也不是安全的单例模式的实现,可以通过反射破坏单例。

懒汉式

只有使用该对象时,才创建之。

package singleTon;

public class LazySingleTon {
    private String slogan;

    private LazySingleTon(String slogan) {
        this.slogan = slogan;
    }

    private static LazySingleTon lazySingleTon;

    public static LazySingleTon getInstance(){
        if(lazySingleTon==null){
             lazySingleTon=new LazySingleTon("懒汉一号准备就绪");
        }
        return lazySingleTon;
    }

    public static void main(String[] args) {
        LazySingleTon lazySingleTon1 = LazySingleTon.getInstance();
        LazySingleTon lazySingleTon2 = LazySingleTon.getInstance();
        System.out.println(lazySingleTon1==lazySingleTon2);//true
    }

}

如下图所示,懒汉式解决了JVM一启动就创建实例对象的问题,但存在线程安全问题。

package singleTon;

public class LazySingleTon {
    private String slogan;

    private LazySingleTon(String slogan) {
        this.slogan = slogan;
        System.out.println(Thread.currentThread().getName());
    }

    private static LazySingleTon lazySingleTon;

    public static LazySingleTon getInstance() {
        if (lazySingleTon == null) {
            lazySingleTon = new LazySingleTon("懒汉一号准备就绪");
        }
        return lazySingleTon;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> LazySingleTon.getInstance(), "thread" + i).start();
        }
        /**
         * 输出结果:
         * thread0
         * thread3
         * thread2
         * thread1
         */
    }

}

DCL(Double Check Lock)

通过加锁解决懒汉式线程安全问题。

package singleTon;

public class DCLSingleTon {
    private String slogan;

    private DCLSingleTon(String slogan) {
        this.slogan = slogan;
        System.out.println(Thread.currentThread().getName());
    }

    //添加 volatile关键字 防止指令重排(dclSingleTon=new DCLSingleTon("DCL懒汉一号准备就绪");)
    private static volatile DCLSingleTon dclSingTon;

    public static DCLSingleTon getInstance(){
        /**
         * 这里有两次判断,逻辑上外部判断可以去除。
         * 外部的判断是为了增加效率,如果instance不为空,就可以直接返回值,没有必要进入同步代码块。
         */
        if(dclSingleTon==null){
            synchronized (DCLSingleTon.class){
                if(dclSingleTon==null){
                    dclSingleTon=new DCLSingleTon("DCL懒汉一号准备就绪");
                }
            }
        }
        return dclSingleTon;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->DCLSingleTon.getInstance(),"thread"+i).start();
        }
         /**
         * 输出结果:
         * thread0
         */

    }

}

上述DCL虽然保证了线程安全,但是也不是安全的单例模式的实现,可以通过反射破坏单例。

public static void main(String[] args) throws Exception {

        //获取DCL的实例
        DCLSingleTon instance = DCLSingleTon.getInstance();

        //通过反射使用DCLSingTon类的构造器创建实例
        Constructor<DCLSingleTon> dclSingleTonConstructor = DCLSingleTon.class.getDeclaredConstructor(String.class);
        //关闭安全检查
        dclSingleTonConstructor.setAccessible(true);
        // 获取实例
        DCLSingleTon reflectInstance = dclSingleTonConstructor.newInstance("反射DCL懒汉一号准备就绪");

        System.out.println(instance==reflectInstance);//false

    }

静态内部类

当静态内部类被使用时,才会被加载,且只会加载一次,在加载时线程是安全的。

package singleTon;

import java.lang.reflect.Constructor;

public class staticInternalSingleTon {

    private staticInternalSingleTon(String slogan) {
        System.out.println(Thread.currentThread().getName());
    }

    // 当静态内部类被使用时,才会被加载,且只会加载一次,在加载时线程是安全的
    public static class Inner {
        private static final staticInternalSingleTon INSTANCE =
            new staticInternalSingleTon("静态内部类一号准备就绪");
    }

    public static staticInternalSingleTon getInstance() {
        return Inner.INSTANCE;
    }

    public static void main(String[] args) throws Exception {
        // 通过反射使用staticInternalSingleTon类的构造器创建实例
        Constructor<staticInternalSingleTon> declaredConstructor = staticInternalSingleTon.class.getDeclaredConstructor(String.class);
        declaredConstructor.setAccessible(true);
        staticInternalSingleTon staticInternalSingleTon = declaredConstructor.newInstance("反射静态内部类一号准备就绪");

        System.out.println(getInstance() == staticInternalSingleTon);//false
    }

}

上述静态内部类虽然保证了线程安全,但是也不是安全的单例模式的实现,可以通过反射破坏单例。

枚举

枚举类在类加载时创建,保证线程安全且不可以通过 (new,clone,反射,序列化)创建枚举实例。

package singleTon;

public enum EnumSingleTon {
    INSTANCE;

    public static EnumSingleTon getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws Exception {
        EnumSingleTon instance1 = EnumSingleTon.getInstance();
        EnumSingleTon instance2 = EnumSingleTon.getInstance();
        System.out.println(instance1.equals(instance2));//true

        /*
        通过反射获取实例
        java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        不允许通过反射创建枚举对象
        Constructor<EnumSingleTon> enumSingTonConstructor = EnumSingleTon.class.getDeclaredConstructor(String.class,int.class);
        enumSingTonConstructor.setAccessible(true);
        EnumSingleTon enumSingTon = enumSingTonConstructor.newInstance();
        System.out.println(instance1.equals(enumSingTon));
         */


    }
}

三、总结

  • 单例模式是23个模式中比较简单的模式,应用也非常广泛,如在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理这些Bean的生命期。
  • 单例模式的核心特点便是一个类仅有一个实例且自行实例化。
  • 单例模式的实现方式应根据具体业务进行设计。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值