单例模式

79 阅读8分钟

单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式的实现方式有两种,一种是饿汉式,另一种是懒汉式。

饿汉式

饿汉式会在类加载的时候进行对象实例化,然后通过方法进行调用。通过代码进行解释

代码如下:

class HungrySingleton{
    private static HungrySingleton hungrySingleton=new HungrySingleton();
    
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}
public class HungrySingletonTest {
    public static void main(String[] args) throws Exception {
     HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton instance1 = HungrySingleton.getInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }
}
/*
结果:
com.itheima.designpattern.Singleton.HungrySingleton@4554617c
com.itheima.designpattern.Singleton.HungrySingleton@4554617c
*/

饿汉式通过一开始就进行实例化的方式来实现单例模式。既然在单线程环境下能够实现单例模式,那么在多线程环境中是否能够保证单例模式呢?我们接下开进行测试!

测试代码:

public class HungrySingletonTest {
    public static void main(String[] args) throws Exception {
     HungrySingleton instance = HungrySingleton.getInstance();
        new Thread(()->{
            System.out.println(HungrySingleton.getInstance());
        }).start();
        new Thread(()->{
            System.out.println(HungrySingleton.getInstance());
        }).start();
    }
}
/*
结果:
com.itheima.designpattern.Singleton.HungrySingleton@688ee48d
com.itheima.designpattern.Singleton.HungrySingleton@688ee48d
*/

通过结果发现,饿汉式在多线程环境下也能够保证单例模式的实现!饿汉式既然在多线程环境下也能够保证单例模式的实现,那么只需要该模式即可,为什么还有这么多种实现方式呢?其实它的缺点也非常明显,由于它是通过在类加载的时候进行对象实例化,然后通过方法进行调用实现单例模式的,那么在不调用该方式来创建单例对象的时候,也会去创建一个对象,会占用额外的内存空间!

懒汉式

懒汉式就是在真正需要使用对象时才去创建该单例类对象。通过代码来进行解释

代码如下:

class LazySingleton{
    private static LazySingleton lazySingleton=null;
    private LazySingleton(){}
    public static LazySingleton getInstance(){
        if (lazySingleton == null) {//判断对象是否创建,创建了就直接使用。
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}
public class LazySingletonTest {
    public static void main(String[] args) {
        LazySingleton instance = LazySingleton.getInstance();
        LazySingleton instance1 = LazySingleton.getInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }
}
/*
结果:
com.itheima.designpattern.Singleton.LazySingleton@4554617c
com.itheima.designpattern.Singleton.LazySingleton@4554617c
*/

懒汉式只有在需要使用到对象的时候才回去创建该对象,如果不需要使用的话它不会被创建。接下来对该实现方式在多线程环境下进行一个测试。

测试代码如下:

public class LazySingletonTest {
    public static void main(String[] args) {
       new Thread(() -> {
            System.out.println(LazySingleton.getInstance());
        }).start();
        new Thread(() -> {
            System.out.println(LazySingleton.getInstance());
        }).start();
    }
}
/*
结果:
com.itheima.designpattern.Singleton.LazySingleton@7f1d78ac
com.itheima.designpattern.Singleton.LazySingleton@21279f82
*/

通过结果发现该实现方式在多线程环境下会出现线程安全问题,那么就需要解决该问题,一旦出现线程安全问题,就需要使用到锁机制来进行线安全保障!

解决方案如下:

class LazySingleton{
    private static LazySingleton lazySingleton=null;
    private LazySingleton(){}
    public static LazySingleton getInstance(){
            synchronized (LazySingleton.class){
                if (lazySingleton == null) {
                    lazySingleton = new LazySingleton();
                }
            }
​
        return lazySingleton;
    }
}

通过锁的机制来保证在多线程环境下的线程安全问题,但是一旦使用到锁机制,那么就会造成一个性能的问题,频繁的加锁释放锁会严重的消耗性能。那么就需要对该方法进行一个性能优化。只有当该对象没有创建的时候才进行加锁,有对象的时候就不需要再进行一个加锁,这样子就会减少加锁释放锁的次数。

代码如下:

class LazySingleton{
    private static LazySingleton lazySingleton=null;
    private LazySingleton(){}
    public static LazySingleton getInstance(){
     if (lazySingleton == null) {
            synchronized (LazySingleton.class){
                if (lazySingleton == null) {
                    lazySingleton = new LazySingleton();
                }
            }
        }
        return lazySingleton;
    }
}

上面这段代码就是DCL懒汉式的实现方式。DCL=Double Check Lock(双重检测锁)。这种方式能够保障多线程环境下线程安全问题和性能问题。

这种方式已经非常好了,但是还是会有问题,就是指令重排问题!

指令重排:在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。

在创建对象的时候会有三个步骤:

1、开辟内存空间

2、内存空间初始化

3、对象指向内存空间

指令重排会在保证结果正确的前提下,有可能会对着三个步骤进行一个重排,如1-3-2这样。这种保证正确的前提是在单线程环境下,如果在多线程环境下指令重排有可能会出现问题。所以需要使用到volatile关键字来禁止指令重排的情况出现。 一个完整的DCL懒汉式代码如下:

class LazySingleton{
    private static volatile LazySingleton lazySingleton=null;
    private LazySingleton(){}
    public static LazySingleton getInstance(){
            if (lazySingleton==null){
                synchronized (LazySingleton.class){
                    if (lazySingleton == null) {
                        lazySingleton = new LazySingleton();
                    }
                }
            }
        return lazySingleton;
    }
}

饿汉式和懒汉式的区别

1、饿汉式是在类加载的时候实例化对象,懒汉式则是在需要的时候才会实例化对象

2、饿汉式在多线程的环境下也是线程安全的,但是懒汉式如果不是DCL的那么就会出现线程安全问题,所以懒汉式的性能会比饿汉式的差

3、饿汉式由于是在类加载的时候就进行实例化所以在不使用该对象的时候就会比较占用空间

以上两种方式就是实现单例模式的主要实现方式,但是这两种方式都有共同的问题,就是会被反射和序列化破坏。

反射方式破坏单例模式

代码如下(用饿汉式的进行举例):

class HungrySingleton{
    private static HungrySingleton hungrySingleton=new HungrySingleton();
​
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}
public class HungrySingletonTest {
    public static void main(String[] args) throws Exception {
        Constructor<HungrySingleton> declaredConstructor = HungrySingleton.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        HungrySingleton hungrySingleton = declaredConstructor.newInstance();
        HungrySingleton hungrySingleton1 = declaredConstructor.newInstance();
        System.out.println(hungrySingleton);
        System.out.println(hungrySingleton1);
    }
}
/*
结果:
com.itheima.designpattern.Singleton.HungrySingleton@4554617c
com.itheima.designpattern.Singleton.HungrySingleton@74a14482
*/

通过反射的机制很容易就将单例模式的实现进行破坏!那么可以解决嘛?我认为是没有什么办法的,反射是无敌的!

序列化方式破坏单例模式

代码如下:

class SerializableSingleton implements Serializable{
    public final long serialVersionUID = 42L;
    private static SerializableSingleton serializableSingleton=new SerializableSingleton();
    private SerializableSingleton(){
​
    }
    public static SerializableSingleton getInstance(){
        return serializableSingleton;
    }
}
public class SerializableSingletonTest {
    public static void main(String[] args) throws Exception {
        SerializableSingleton instance = SerializableSingleton.getInstance();
        //将单例对象写入到文件中
ObjectOutputStream objectOutputStream=new ObjectOutputStream(newFileOutputStream("serializableSingleton"));
        objectOutputStream.writeObject(instance);
        objectOutputStream.close();
        //将单例对象从文件中读取出来
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("serializableSingleton"));
        SerializableSingleton o = (SerializableSingleton)objectInputStream.readObject();
        System.out.println(o==instance);
    }
}
/*
结果:
false
*/

通过序列化的方式来对单例模式进行破坏也非常容易,那么这种方式可以被解决吗?答案是肯定的!只需要实现readResolve()方法即可解决这种问题!

class SerializableSingleton implements Serializable{
    public final long serialVersionUID = 42L;
    private static SerializableSingleton serializableSingleton=new SerializableSingleton();
    private SerializableSingleton(){
​
    }
    public static SerializableSingleton getInstance(){
        return serializableSingleton;
    }
    Object readResolve() throws ObjectStreamException{
        return serializableSingleton;
    }
}
public class SerializableSingletonTest {
    public static void main(String[] args) throws Exception {
        SerializableSingleton instance = SerializableSingleton.getInstance();
        //将单例对象写入到文件中
ObjectOutputStream objectOutputStream=new ObjectOutputStream(newFileOutputStream("serializableSingleton"));
        objectOutputStream.writeObject(instance);
        objectOutputStream.close();
        //将单例对象从文件中读取出来
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("serializableSingleton"));
        SerializableSingleton o = (SerializableSingleton)objectInputStream.readObject();
        System.out.println(o==instance);
    }
}
/*
结果:
true
*/

通过这两种方式都可以对单例模式进行破坏,那么就有没有不让这两种方式破坏的单例模式实现方式呢?答案是有的!就是使用枚举类进行单例模式实现!

枚举类型实现

public enum Singleton {
    intsance;
}
public class EnumSingletonTest {
    public static void main(String[] args) {
        Singleton intsance = Singleton.intsance;
        Singleton intsance1 = Singleton.intsance;
        System.out.println(intsance==intsance1);
    }
}
/*
结果:
true
*/

使用枚举类型的方式来实现单例模式非常的简单,并且这种方式能够防止被反射和序列化进行破坏。通过源码层面来查看原因

反射:

通过以上的源码发现是禁止通过反射的机制来创建枚举类型!

序列化:

代码如下:

public enum Singleton implements Serializable {
    intsance;
}
public class EnumSingletonTest {
    public static void main(String[] args) throws Exception{
        Singleton intsance = Singleton.intsance;
        //输出
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("EnumSingleton"));
        objectOutputStream.writeObject(intsance);
        //输入
        ObjectInputStream enumSingleton = new ObjectInputStream(new FileInputStream("EnumSingleton"));
        Singleton o = (Singleton)enumSingleton.readObject();
        System.out.println(o==intsance);
    }
}
/*
结果:
true
*/

使用枚举类实现单例模式的优势:

1、使用枚举类型实现单例模式更加简单方便

2、防止反射和序列化破坏单例模式

总结:

1、单例模式两种实现方式:饿汉式、懒汉式

2、饿汉式和懒汉式都会被序列化和反射破坏,枚举类就不会被序列化和反射破坏

3、懒汉式中为了防止指令重排导致的不可预知的错误,需要使用到volatile关键字进行修饰

4、单例模式很容易理解,但是解决单例模式被破坏的方式方法则需要好好分析理解