设计模式-单列模式

定义

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

单例模式的结构和实现

单列模式的结构

单例模式的主要角色如下。

  • 单例类:包含一个实例且能自行创建这个实例的类。
  • 访问类:使用单例的类。

单列模式的实现

单列模式的实现分为两种:

  • 饿汉模式:只要访问单列类就创建对象。
  • 赖汉模式:只有获取对象实例的时候才会创建对象。
饿汉模式
public class Singleton {
    //私有化构造器
    private Singleton(){}
    //private static Singleton instance = new Singleton();
    private static Singleton instance;
    static {
        instance = new Singleton();
    }

    public static Singleton getInstance(){
        return instance;
    }
}

上面使用类变量或者静态代码块创建对象,对象随着类的创建而创建。且保证线程安全,同时没有指令重排序的问题。这是JVM可以保证的。如果在系统内存足够大的情况下。是一个不错的选择。

赖汉模式

首先我们看下面这段代码:

public class Singleton {
    //私有化构造器
    private Singleton() {
    }

    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

上段代码在单线程情况下是没有问题的,但在多线程状态下就有问题。可以用代码测试下

public class MainTest {
    public static void main(String[] args){
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                System.out.println(Singleton.getInstance());
            });
            thread.start();
        }

    }
}

输出结果如下:
在这里插入图片描述
开启了10个线程,第一次运行就得到了不同对象。

于是我们考虑给获取实例的方法加锁。直接在方法上加synchronized可以解决问题,但是锁粒度太大。影响程序性能。所以我们让锁的粒度更小些,如下面这段代码:

public class Singleton {
    //私有化构造器
    private Singleton() {
    }

    private static Singleton instance;

    public  static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

这样在对象之前加锁。可以提高程序的执行效率。但是我们考虑下上述代码在高并发下一定是单列吗。答案是否定的。如果面试时问到赖汉模式,你这么回答的话。可能这道题只能给你个2分(总分10分的话)。
我们还是用上述的MainTest代码测试下:
在这里插入图片描述
在高并发下还是得到了不同的对象。所以上述的例子还需改进。于是有了双重检查锁,就是在加锁的前后进行一次null判断,代码如下:

public class Singleton {
    //私有化构造器
    private Singleton() {
    }

    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这样我们经过测试貌似得到的对象都是同一个对象。通过上述的MainTest我测试了多次得到的对象多是同一个,线程放大点貌似也是。但是上述双重检查锁还是有问题的,JVM创建对象时可能进行指令重排序。
JVM创建对象时分为三步:

  1. 为对象分配内存
  2. 初始化构造器
  3. 将对象指向分配的内存地址。

上述的这个顺序在双重检查锁下是没有问题的,但是如果2,3交换呢。先分配内存地址在初始化构造器。如果先进行步骤三时别的线程这时正好进来开始检查,误判instance已经实例化了,但是该对象还没有进行初始化构造器。别的线程会直接返回对象引用,这时就会出现比较奇怪的异常。所以我们使用关键字volatile防止指令重排序,保证对象正确的创建。代码如下:

public class Singleton {
    //私有化构造器
    private Singleton() {
    }

    private volatile static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

经过多次改进之后我们得到的就是一个比较完整的获取单列对象的双重检查锁的赖汉模式,且是线程安全的。这是一个比较好的实现方式。Spring底层大量使用了这种方式。如容器中获取单列的getSingleton方法。
还有一种更简洁的方式,就是利用静态内部类的特性。就是JVM在加载外部类是是不会加载静态内部类,只有在访问内部类的静态属性和方法时才会加载。代码如下:

public class Singleton {
    //私有化构造器
    private Singleton(){}
    
    private static class RealSingleton{
        private static final Singleton instance = new Singleton();
    }
    
    public static Singleton getInstance(){
        return RealSingleton.instance;
    }
}

这是一种优秀且比较简洁的实现,对于类变量JVM保证创建时是线程安全的。且是已赖汉模式创建,节省了内存空间。
还有一种就是通过枚举类创建单列对象,如下代码:

public enum Singleton {
    instance();
}

非常之简洁,且是线程安全,只会被加载一次。这是枚举类的特性。也是唯一不会被破坏的单列模式。至此单列模式的几种方式讲完了。

单列模式的破坏

反射对单列的破坏

以静态内部类为例,测试代码如下:

public class Singleton {
    //私有化构造器
    private Singleton(){}
    
    private static class RealSingleton{
        private static final Singleton instance = new Singleton();
    }
    
    public static Singleton getInstance(){
        return RealSingleton.instance;
    }
}

public class MainTest {
    public static void main(String[] args)  throws Exception{
        Singleton instance = getInstance();
        Singleton instance1 = getInstance();

        System.out.println(instance1 == instance);
    }

    public static Singleton getInstance() throws Exception {
        Class clz = Singleton.class;
        Constructor cos = clz.getDeclaredConstructor();
        cos.setAccessible(true);
        Object o = cos.newInstance();
        return (Singleton) o;
    }
}

结果为false。也就是说通过反射我们得到不同的对象非单列。其实就是私有化构造器对反射是没用的。反射还是可以通过构造器创建对象。这时我们可以在构造器中增加一些判断。构造器改造如下:

public class Singleton {
    private static boolean flag = false;
    //私有化构造器
    private Singleton(){
        synchronized (Singleton.class){
            if(flag){
                throw new RuntimeException();
            }
            flag = true;
        }
    }
    private static class SingletonHolder{
        public static final Singleton instance = new Singleton();
    }
    //对外提供静态方法获取对象
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

这样反射创建对象时如果对象已经被创建过就会抛运行时异常,从而避免对象被多次创建。

对象序列化对单列的破坏

还是以静态类创建单列对象的方式来测试(需要实现Serializable接口),测试代码如下:

public class MainTest {
    public static void main(String[] args) throws Exception {
        Singleton instance = Singleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\lenovo\\Desktop\\cs.txt"));
        //将对象写入文件中
        oos.writeObject(instance);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\lenovo\\Desktop\\cs.txt"));
        Singleton instance1 = (Singleton) ois.readObject();

        System.out.println(instance == instance1);

    }
}

结果为false,也就是说。对象流写入的和读到的不是一个对象。读取时重新创作了对象。这时我们需要在单列类中加入readResolve方法。代码如下:

//解决反序列化对单列模式的破坏 ,反序列化时,会自动调用该方法,将该方法的
    public Object readResolve(){
        return RealSingleton.instance;
    }

通过源码可以看到输入流读取对象时判断有无此方法来确定是否创建新的对象。
至此单列模式全部结束,谢谢大家观看```````

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值