23种设计模式:单例模式

单例模式(Singleton Pattern)是一种创建型设计模式。
它确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

1. 饿汉式

所谓饿汉式,就是直接创建出类的实例化,然后用private私有化,对外只用静态方法暴露。

1.1. 静态变量

步骤

  1. 构造器私有化
  2. 类的内部创建对象
  3. 对外暴露一个静态的共有方法获取该对象

Singleton类:

public class Singleton {

    //1.私有化构造函数
    private Singleton() {
    }

    //2.内部创建对象实例
    private static final Singleton INSTANCE = new Singleton();

    //3.对外公有的静态方法获取该对象
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

1.2. 静态代码块

将类的实例化放到静态代码块中的写法,基本同上。
Singleton类:

public class Singleton {

    //1.私有化构造函数
    private Singleton() {
    }

    //2.内部创建对象实例
    private static final Singleton INSTANCE;
    static {
        INSTANCE = new Singleton();
    }

    //3.对外公有的静态方法获取该对象
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

2. 懒汉式

所谓懒汉式,就是在需要调用的时候再创建类的实例化。

2.1 线程不安全

起到了懒加载效果,但是只能在单线程使用,多线程会不安全,因为当多个线程并发同时判断instance为空时,就会相应的实例化多个对象。
Singleton类:

public class Singleton {

    //1.私有化构造函数
    private Singleton() {
    }

    //2.内部创建对象实例
    private static Singleton instance;

    //3.对外公有的静态方法获取该对象
    public static Singleton getInstance() {
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }
}

2.2 线程安全

上述线程不安全,通过加同步锁(synchronized)的机制进行优化,保证每次只有一个线程可以操作当前对象,确保了线程安全。
这样虽然解决了线程安全,但实例化操作只做一次,而获取实例(即getInstance)的操作是很多次的,把调用的方法加上同步,会大大降低效率。
Singleton类:

public class Singleton {

    //1.私有化构造函数
    private Singleton() {
    }

    //2.内部创建对象实例
    private static Singleton instance;

    //3.对外公有的静态方法获取该对象
    public static synchronized Singleton getInstance() {
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }
}

2.3. 双重检查锁

上述代码效率低,因此在同步前判断一下有没有实例化就可以了,若没有实例化则用同步方法new一个,否则直接return即可。即所谓的双重检查。
需要用到关键字volatile,防止指令重排。如果不用volatile关键字,就会和线程不安全情形一样,在if判断那会有并发。
Singleton类:

public class Singleton {

    //1.私有化构造函数
    private Singleton() {
    }

    //2.内部创建对象实例
    private static volatile Singleton instance;

    //3.对外公有的静态方法获取该对象
    public static Singleton getInstance() {
        if (null == instance) {
            synchronized (Singleton.class) {
                if (null == instance) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

(1)双重检查锁模式下,为什么要进行两次判断?
假设有两个线程AB。两个线程都去请求单例模式下类的实例,当第一个判断的时候,两个线程都会进入判断代码块中进行锁的抢占,最终A抢占到了锁,那么B只能在加锁的代码块外部进行等候,这个时候A创建了对象的实例,完成功能后归还了锁,这个时候线程B马上抢占到了锁,然后进入内部代码块,假设在这里没有第二次判断的话,线程B就会再次创建一个新的对象,所以,要在这里再加一次判断。

(2)双重检查锁模式下,为什么要使用volatile关键字?
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,但是,JVM在实例化对象的时候会进行优化和指令重排序操作,在多线程的情况下,就可能会出现空指针问题。

对于上述对象的创建主要分为三个部分:
①:分配对象的内存空间。
②:初始化对象。
③:设置instance指针指向对象所在的内存空间。

为了提高性能JVM在实例化对象的时候会进行优化和指令重排序操作,就是说有可能将上述的第 8 行和第 11 行代码进行顺序的交换。
就是说当上面A线程得到锁以后,这时候还没有初始化对象,就先设置了instance指针指向了对象所在的内存空间,A线程在设置了instance指针指向了对象所在的内存空间以后就归还了锁,线程B这个时候拿到锁以后,检查到对象不为空,直接返回了线程A创建的对象,但是这个时候线程A还没有完成对象的初始化,所以就导致了线程B拿到的对象是一个空对象,就会出现空指针的问题。

因此解决上述的问题,只需要使用volatile 关键字, volatile关键字可以保证可见性和有序性,这个关键字禁止了对当前修饰的变量上下文重排序,保证了方法的可靠性。

2.4. 静态内部类

Singleton类:

public class Singleton {

    //1.私有化构造函数
    private Singleton() {
    }

    //2.静态内部类,包含一个静态属性:Singleton
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    //3.对外公有的静态方法获取该对象,直接返回 SingletonInstance.INSTANCE
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证Singleton类的唯一性。
因此静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

3. 单例模式存在的问题

3.1. 单例模式被破坏

在上述定义的单例类(Singleton)中正常的使用的情况下只可以同时只有一个对象存在,但是存在着一些操作可以破坏这种现象,使得上述单例模式可以创建多个对象(枚举方式除外)。

这两种方式,分别是序列化和反射:
(1)序列化反序列化破坏单例模式
在这里使用性能较好的内部类方式的单例模式,来检验一下,序列化与反序列化是否会创建不同的对象。
Singleton类:

public class Singleton implements Serializable {

    //1.私有化构造函数
    private Singleton() {
    }

    //2.静态内部类,包含一个静态属性:Singleton
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    //3.对外公有的静态方法获取该对象,直接返回 SingletonInstance.INSTANCE
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Test类:

public class Main {

    public static void deserialization() throws IOException, ClassNotFoundException {
        //1.反序列化
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        //2.将类转化
        objectOutputStream.writeObject(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
        //3.读出类,变为一个新的类
        Singleton singleton = (Singleton) objectInputStream.readObject();
        System.out.println(singleton);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        deserialization();
    }
}

运行结果:
在这里插入图片描述
根据运行结果,表明序列化和反序列化已经破坏了单例设计模式只有一个对象存在的原则。

(2)反射破坏单例模式
还是使用性能较好的内部类方式的单例模式,来检验一下,序列化与反序列化是否会创建不同的对象。
Singleton类:

public class Singleton implements Serializable {

    //1.私有化构造函数
    private Singleton() {
    }

    //2.静态内部类,包含一个静态属性:Singleton
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    //3.对外公有的静态方法获取该对象,直接返回 SingletonInstance.INSTANCE
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Test类:

public class Main {

    public static void reflex() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        System.out.println(Singleton.getInstance());
        //1.反射破坏
        Class<Singleton> hungrySingletonClass = Singleton.class;
        Constructor<Singleton> declaredConstructor = hungrySingletonClass.getDeclaredConstructor();
        //2.设置私有可调用
        declaredConstructor.setAccessible(true);
        System.out.println(declaredConstructor.newInstance());
    }

    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        reflex();
    }
}

运行结果:
在这里插入图片描述
根据运行结果,表明反射也破坏了单例设计模式只有一个对象存在的原则。

3.2. 原因分析

对于序列化与反序列化破坏单例模式的问题,主要是通过readObject()方法,出现了破坏单例模式的现象,主要是因为这个方法最后会通过反射调用无参数的构造方法创建一个新的对象,从而每次返回的对象都不一致。

对于反射破坏单例模式是因为单例模式通过setAccessible(true)指示反射的对象在使用时,取消了Java语言访问检查,使得私有的构造函数能够被访问。而单例模式的设计在于只保留一个公有静态函数来获取唯一的实例,其他方法(构造函数)或字段为私有,外界不能访问。而反射破坏了这一原则,它突破了构造函数私有的限制,可以获取单例类的私有构造函数并使用其创建多个对象。

3.3. 问题解决

(1)序列化、反序列方式破坏单例模式的解决方法
readObject()方法的调用栈的底层方法中有这么两个方法:
hasReadResolveMethod:表示如果实现了serializable或者externalizable接口的类中包含readResolve则返回true
invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve()方法。
因此,原理也就清楚了,主要在Singleton中定义readResolve()方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。
Singleton类:

public class Singleton implements Serializable {

    //1.私有化构造函数
    private Singleton() {
    }

    //2.静态内部类,包含一个静态属性:Singleton
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    //3.对外公有的静态方法获取该对象,直接返回 SingletonInstance.INSTANCE
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    //4.防止序列化、反序列化方式破坏单例
    private Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}

(2)反射方式破解单例的解决方法
反射是一种暴力获取对象实例的方法,因为他可以直接访问private修饰的构造函数,所以在对于反射方式破坏单例模式的问题上只能采取被动的防御,既然你能访问我的构造函数,我就在我的构造函数中建立防御机制,不让你通过我的构造函数创建多个实例对象。
Singleton类:

public class Singleton implements Serializable {

    private static volatile boolean flag = false;

    //1.私有化构造函数
    private Singleton() {
        synchronized (Singleton.class) {
            if (flag) {
                throw new RuntimeException();
            }
            flag = true;
        }
    }

    //2.静态内部类,包含一个静态属性:Singleton
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    //3.对外公有的静态方法获取该对象,直接返回 SingletonInstance.INSTANCE
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

4. 枚举

其实,使用枚举也能实现单例模式,不仅能避免多线程同步问题,也能防止反序列化重新创建新的对象。
Singleton类:

public enum Singleton {

    INSTANCE
}

Test类:

public class Main {

    public static void main(String[] args) {
        Singleton instance1 = Singleton.INSTANCE;
        Singleton instance2 = Singleton.INSTANCE;

        System.out.println(instance1.hashCode());
        System.out.println(instance2.hashCode());
    }
}

运行结果:
在这里插入图片描述
(1)关于枚举类单例模式为什么不会被破坏
其实是因为java的底层做了很多的方法来防止枚举类单例模式被破坏,其中一点就是无法创建枚举类的实例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值