Java设计模式之创建型模式——单例模式

一、概述

        单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,属于创建型模式。用于确保一个类只有一个实例,并提供一个全局的访问点以获取该实例,常用于需要全局访问点的情况,例如配置文件、日志记录器、线程池等。

        单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。  

单例设计模式两种分类:

饿汉式:类加载就会导致该单实例对象被创建

懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

 下图是两种单例模式方式的实现方式:

 单例模式的优点包括:

提供了全局唯一的实例,方便在系统中共享和访问对象。

避免了频繁创建和销毁对象的开销。

提供了对唯一实例的严格控制,确保所有代码都使用同一个实例。

 单例模式的缺点包括:

单例模式一般会使用静态成员变量,在多线程环境下可能存在并发访问安全问题,需要额外考虑线程安全性。

单例模式通常会和全局状态紧耦合,可能会引入难以测试和调试的问题。

单例模式的生命周期由程序控制,可能导致资源无法及时释放。

单例模式的使用会增加代码的复杂性,降低了代码的可扩展性。


二、饿汉式的实现

2.1 方式一:静态变量方式

        在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

        TIP:静态变量随着类的加载而存在随着类的消失而消失。

/**
 * 饿汉式:静态变量创建类的对象
 */
public class Singleton {

    //构造方法私有化,只有该类本身可以调用,其他类无法调用构造方法创建该类对象
    private Singleton() {
    }

    /**
     * 静态变量创建该类对象
     * 静态变量随着类的加载而存在随着类的消失而消失,静态变量位于方法区。
     * 也就是这个变量已经初始化为该类对象,此后就直到该类销毁才会结束,否则就一直存在这一个。
     */
    private static Singleton instance = new Singleton();

    //提供对外访问的方法,只用调用这个方法就可以返回这个类的实例。
    public static Singleton getInstance(){
        return instance;
    }

}

2.2 方式二:静态代码块方式

        该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1基本上一样,当然该方式也存在内存浪费问题。

        静态代码块在类初始化时执行,并且只会执行一次。

/**
 * 饿汉式:静态代码块创建类的对象
 */
public class Singleton {

    //构造方法私有化,只有该类本身可以调用,其他类无法调用构造方法创建该类对象
    private Singleton(){};

    /**
     * 静态变量创建该类对象
     * 静态变量随着类的加载而存在随着类的消失而消失,静态变量位于方法区。
     * 但是此时暂时不进行初始化,而是在初始化该类时执行静态代码块进行初始化
     */
    private static Singleton instance;

    /**
     * 只会随着类创建执行一次
     * java中我们通常将static修饰的代码块,称为静态代码块,随类存在,仅在类初始化时执行一次
     * 类在什么时候初始化:
     * 1)创建类的实例,也就是new一个对象
     * 2)访问某个类或接口的静态变量,或者对该静态变量赋值
     * 3)调用类的静态方法
     * 4)反射(Class.forName(“com.qianhan.load”))
     * 5)初始化一个类的子类(会首先初始化子类的父类)
     * 6)JVM启动时标明的启动类,即文件名和类名相同的那个类
     * 若无Static修饰则称为非静态代码块,随对象存在,创建几个对象就执行几次。
     */
    static {
        instance = new Singleton();
    }

    //提供对外访问的方法,只用调用这个方法就可以返回这个类的实例。
    public static Singleton getInstance(){
        return instance;
    }
}

2.3 方式三:静态内部类方式

        静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

        第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

/**
 * 饿汉式:静态内部类创建类的对象
 */
public class Singleton {

    //构造方法私有化,只有该类本身可以调用,其他类无法调用构造方法创建该类对象
    private Singleton(){}

    /**
     * 静态内部类
     * JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载
     */
    private static class SingletonHolder{
        //常量
        //静态属性由于被static 修饰,保证只被实例化一次,并且严格保证实例化顺序。
        private static final Singleton INSTANCE = new Singleton();
    }

    //提供对外访问的方法,只用调用这个方法就可以返回这个类的实例。
    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

        静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

2.4 方式四:枚举方式

        枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

//枚举类型是线程安全的,并且只会装载一次
//若枚举类只有一个枚举值,则可以当作单态设计模式使用
//枚举类型是所有单例实现中唯一一种不会被破坏的单例实现模式
public enum Singleton {
    INSTANCE;
}

测试

        通过调用getInstance()方法获取单例对象,判断两个对象地址是否相同。以上四种创建方式的返回结果均为true,表示以上创建方式只存在一个对象,为单例模式。

    @Test
    public void test(){
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance1 == instance2); //返回ture
    }

三、懒汉式的实现

        懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。否则则先执行实例化操作。

3.1 方式一:对方法进行加锁

① 初始方式:线程不安全

       在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。

该图来源:CSDN博客

/**
 * 懒汉式:线程不安全
 */
public class Singleton {

    //构造方法私有化,只有该类本身可以调用,其他类无法调用构造方法创建该类对象
    private Singleton(){}

    /**
     * 静态变量创建该类对象
     * 静态变量随着类的加载而存在随着类的消失而消失,静态变量位于方法区。
     * 但是此时暂时不进行初始化,而是在调用该类时在执行getInstance()方法进行初始化
     */
    private static Singleton instance;

    /**
     * 只用在使用该类时,调用该方法才会创建类,即懒汉式
     * 但是该方式可能出现并发问题,即线程不安全
     */
    public static Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }

}

② 改进方式:线程安全

        该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低,每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

/**
 * 懒汉式:线程安全
 */
public class Singleton {

    //构造方法私有化,只有该类本身可以调用,其他类无法调用构造方法创建该类对象
    private Singleton(){}

    /**
     * 静态变量创建该类对象
     * 静态变量随着类的加载而存在随着类的消失而消失,静态变量位于方法区。
     * 但是此时暂时不进行初始化,而是在调用该类时在执行getInstance()方法进行初始化
     */
    private static Singleton instance;

    /**
     * 只用在使用该类时,调用该方法才会创建类,即懒汉式
     * 此方法进行了加锁,是线程安全的。
     * 但是,由于对方法加锁,导致该方法的执行效果特别低
     */
    public static synchronized Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

3.2 方式二:对方法内部进行加锁(双重检查锁)

① 初始方式:空指针异常

        对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式

         双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,下面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

创建一个对象,在JVM中会经过三步:

(1)为singleton分配内存空间

(2)初始化singleton对象

(3)将singleton指向分配好的内存空间、

指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能。

 该图来源:CSDN博客

//懒汉式:空指针问题
//在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作
public class Singleton {

    //构造方法私有化,只有该类本身可以调用,其他类无法调用构造方法创建该类对象
    private Singleton(){}

    /**
     * 静态变量创建该类对象
     * 静态变量随着类的加载而存在随着类的消失而消失,静态变量位于方法区。
     * 但是此时暂时不进行初始化,而是在调用该类时在执行getInstance()方法进行初始化
     */
    private static Singleton instance;

    /**
     * 只用在使用该类时,调用该方法才会创建类,即懒汉式
     */
    public static Singleton getInstance(){
        if (instance == null){
            synchronized (Singleton.class){
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

      

② 改进方式:volatile关键字

Volatile关键字的作用主要有如下两个:

  • 线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

  • 顺序一致性:禁止指令重排序

使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生NPE异常了;

使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量

//懒汉式:
public class Singleton {

    //构造方法私有化,只有该类本身可以调用,其他类无法调用构造方法创建该类对象
    private Singleton(){}

    /**
     * 静态变量创建该类对象
     * 静态变量随着类的加载而存在随着类的消失而消失,静态变量位于方法区。
     * 但是此时暂时不进行初始化,而是在调用该类时在执行getInstance()方法进行初始化
     *
     * 需要加上volatile关键字,该关键字可以保证可见性和有序性。
     * Volatile关键字的作用主要有如下两个:
     * 1.线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
     * 2.顺序一致性:禁止指令重排序
     */
    private static volatile Singleton instance;

    /**
     * 只用在使用该类时,调用该方法才会创建类,即懒汉式
     * 此方法进行了加锁,是线程安全的。
     */
    public static Singleton getInstance(){
        if (instance == null){
            synchronized (Singleton.class){
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

四、破坏单例模式的方式及解决办法

        破坏单例模式: 使上面定义的单例类(Singleton)可以创建多个对象(枚举方式除外)。有两种方式,分别是序列化反射。

4.1 破坏方式一:序列化和反序列化

/**
 * 破坏单例模式方式一:序列化和反序列化
 * 方式:把Singleton对象写入到文件中,再从文件中读取出来
 * 这种方式所获得的对象地址不同,即破坏了单例模式
 */
public class Destroy {
    public static void main(String[] args) throws Exception {
        //writeObject2File();
        Singleton singleton1 = readObjectFromFile();
        Singleton singleton2 = readObjectFromFile();
        System.out.println(singleton1 == singleton2);//false
    }

    //向文件中写对象数据
    public static void writeObject2File() throws Exception {
        //1,获取Singleton对象
        Singleton instance = Singleton.getInstance();
        //2,创建对象输出流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src/main/java/com/qianhan/pattern/singleton/quetion/Demo1/object.txt"));
        //3,写对象
        oos.writeObject(instance);
        //4,释放资源
        oos.close();
    }

    private static Singleton readObjectFromFile() throws Exception {
        //1,创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src/main/java/com/qianhan/pattern/singleton/quetion/Demo1/object.txt"));
        //2,读取对象
        Singleton instance = (Singleton) ois.readObject();
        //释放资源
        ois.close();
        return instance;
    }
}

解决办法:重写readResolve()方法

/**
 * 饿汉式:静态变量创建类的对象
 */
public class Singleton implements Serializable {

    private Singleton() {
    }

    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return instance;
    }

    /**
     * 下面是为了解决序列化反序列化破解单例模式
     */
    private Object readResolve() {
        return instance;
    }
}

4.2 破坏方式二:反射

/**
 * 破坏单例模式方式二:反射
 * 方式:通过反射方式创建对象,得到的对象不是同一个
 */
public class Destroy {
    public static void main(String[] args) throws Exception {
        //1,获取Singleton的字节码对象
        Class clazz = Singleton.class;
        //2,获取无参构造方法对象
        Constructor cons = clazz.getDeclaredConstructor();
        //3,取消访问检查
        cons.setAccessible(true);
        //4,创建Singleton对象
        Singleton s1 = (Singleton) cons.newInstance();
        Singleton s2 = (Singleton) cons.newInstance();

        System.out.println(s1 == s2); //false,说明破坏了单例模式
    }
}

解决办法:在构造方法进行判断,如果已经存在该对象,就抛出异常

/**
 * 破坏单例模式方式二解决办法:反射
 * 方式:在构造方法处进行判断,如果已经有了对象就抛出异常
 */
public class Destroy {
    public static void main(String[] args) throws Exception {
        //1,获取Singleton的字节码对象
        Class clazz = Singleton.class;
        //2,获取无参构造方法对象
        Constructor cons = clazz.getDeclaredConstructor();
        //3,取消访问检查
        cons.setAccessible(true);
        //4,创建Singleton对象
        Singleton s1 = (Singleton) cons.newInstance();
        Singleton s2 = (Singleton) cons.newInstance();

        System.out.println(s1 == s2); //false,说明破坏了单例模式
    }
}

  • 20
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值