创建型模式之单例模式(相信你看完会对单例模式有新的认识)

单例模式的定义与特点

单例(Singleton)模式的定义:
是指确保一个类在任何情况下都绝对只有一个实例,隐藏其所有的构造方法,并提供一个全局访问点。属于创建型模式。

单例模式有 3 个特点:
1.单例类只有一个实例对象;
2.该单例对象必须由单例类自行创建;
3.单例类对外提供一个访问该单例的全局访问点;

单例模式的结构

单例模式是设计模式中最简单的模式之一,但也是面试时常被问到的。通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。

单例模式的主要角色如下:
单例类:包含一个实例且能自行创建这个实例的类。
访问类:使用单例的类。

单例模式的实现

说起单例模式的实现,我们首先会想到的就是“饿汉式"和"懒汉式"下面就详细的讲讲。

饿汉式单例

特点:在单例类首次加载的时候就创建实例。
优点:没有任何锁,执行效率高,性能高。
缺点:在某些情况下,可能会造成内存的浪费,因为不管你用不用,它都会在类加载时创建一个对象。

常见的饿汉式单例写法:

/**
 * 常见的饿汉式单例写法
 */
public class HungrySingleton {
    //类加载时就创建HungrySingleton这个实例对象
    private static final  HungrySingleton hungrySingleton = new HungrySingleton();
    //私有化构造函数
    private HungrySingleton(){}
    //提供全局访问点
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

静态代码块写法:

/**
 * 静态代码块写法
 * 看来更有逼格,其实和上边的差不多
 */
public class HungryStaticSingleton {
    //类加载时就创建HungrySingleton这个实例对象
    private static final  HungryStaticSingleton hungryStaticSingleton;
    static {
         hungryStaticSingleton = new HungryStaticSingleton();
    }
    //私有化构造函数
    private HungryStaticSingleton(){}
    //提供全局访问点
    public static HungryStaticSingleton getInstance(){
        return hungryStaticSingleton;
    }
}

因为饿汉式的缺点,为了避免内存造成不必要的浪费,所以出现懒汉式单例。

懒汉式单例

特点:在被外部类调用时才创建实例,解决了饿汉式的内存浪费。
优点:节省内存,减少不必要的内存浪费。

1.最简单的懒汉式写法:
缺点:线程不安全

/**
 * 最简单的懒汉式
 */
public class LazySimpleSingleton {

    private static LazySimpleSingleton lazySimpleSingleton;
    //私有化构造函数
    private LazySimpleSingleton(){}
    //提供全局访问点,用的时候才创建实例
    public static LazySimpleSingleton getInstance(){
        if (lazySimpleSingleton == null){
            lazySimpleSingleton = new LazySimpleSingleton();
        }
        return lazySimpleSingleton;
    }
}

线程破坏懒汉式单例的事故现场:

测试代码:

public class LazySimpleSingletonTest {
    @Test
    public void test1(){
        new Thread(()->{
            System.out.println(LazySimpleSingleton.getInstance());}
            ).start();
        new Thread(()->{
            System.out.println(LazySimpleSingleton.getInstance());}
            ).start();
    }
}

多次运行发现两条线程会有可能会创建出不同的实例对象(如下图),这就违背了单例模式的定义了。
于是产生了懒汉式的第二种写法(加锁)
在这里插入图片描述
2.懒汉式第二种写法(加锁):
解决了线程不安全问题。
缺点:性能低,加锁后导致并行变串行

public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton;
    //私有化构造函数
    private LazySimpleSingleton(){}
    //提供全局访问点
    public static LazySimpleSingleton getInstance(){
    	//加锁,解决线程不安全问题
        synchronized (LazySimpleSingleton.class) {
            if (lazySimpleSingleton == null) {
                lazySimpleSingleton = new LazySimpleSingleton();
            }
        }
        return lazySimpleSingleton;
    }
}

3.懒汉式第三种写法(双重检查锁):
解决了线程不安全问题的同时,也解决了性能低的问题。
注意:变量lazySimpleSingletonvolatile 修饰。因为线程中是存在指令重排序的问题,变量定义的时候会创建一块内存,而创建实例的时候也会创建一块内存,变量要指向创建实例的内存。线程运行中会导致这些顺序发生变化,也就是指令重排序的问题。所以加volatile关键字修饰保证有序性。
缺点:两个if判断导致代码看起来不是那么的优雅,可读性不高。

public class LazySimpleSingleton {
    private static volatile LazySimpleSingleton lazySimpleSingleton;
    //私有化构造函数
    private LazySimpleSingleton(){}
    //提供全局访问点
    public static LazySimpleSingleton getInstance(){
        //检查是否需要加锁阻塞
        if (lazySimpleSingleton == null){
            synchronized (LazySimpleSingleton.class) {
                //检查是否需要创建新的实例
                if (lazySimpleSingleton == null) {
                    lazySimpleSingleton = new LazySimpleSingleton();
                }
            }
        }
        return lazySimpleSingleton;
    }
}

4.懒汉式第四种写法(静态内部类):
特点:利用Java语法的特点,写法优雅,性能高,懒加载避免了内存浪费。看似已经非常完美啦。
解释:因为它创建实例是在静态内部类中,而静态内部类在被使用到的时候才会被加载,所以说避免了内存浪费。

/**
 * 懒汉式静态内部类写法
 */
public class LazyStaticInnerClassSimpleSingleton {
    //私有化构造函数
    private LazyStaticInnerClassSimpleSingleton(){}
    //全局访问点
    private static LazyStaticInnerClassSimpleSingleton getInstance(){
        return StaticInner.Instance;
    }
    //静态内部类,创建实例
    private static class StaticInner{
        private static final LazyStaticInnerClassSimpleSingleton Instance = new LazyStaticInnerClassSimpleSingleton();
    }
}

上边说到的懒汉式静态内部类写法,看似已经很完美了。其实不然,上边所说到的所有单例写法其实有两个共同的缺点:
1.能够被反序列化破坏
2.能够被反射破坏

反序列化破坏单例的事故现场

序列化:就是把内存中对象的状态转化为字节码的形式,把字节码通过IO输出流写到磁盘上,持久化保存下来。
反序列化:就是将持久化的字节码内容,通过IO输入流读取到内存中,转化成一个实例对象。
这里咱们就拿常见的饿汉式单例写法来演示:

测试代码:

	@Test
    public void test(){
        HungrySingleton instance1 = HungrySingleton.getInstance();
        try {
            //将instance1对象 从内存写入到硬盘
            FileOutputStream fos = new FileOutputStream("HungrySingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance1);
            oos.flush();
            oos.close();

            //从硬盘读取到内存
            FileInputStream fis = new FileInputStream("HungrySingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            Object instance2 = ois.readObject();
            ois.close();

            //比较instance1和instance2是否是同一个实例
            System.out.println(instance1 );
            System.out.println(instance2);
            System.out.println(instance1 == instance2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

运行结果: 可以发现,反序列化出来的对象实例和全局访问点获取出来的对象实例不相同,即违背了单例原则。
在这里插入图片描述

解决办法:其实很简单,只需要在单例类中加一个方法:
在这里插入图片描述
再次看运行结果
在这里插入图片描述

反射破坏单例的事故现场

这里咱们就拿懒汉式静态内部类写法来演示:

测试代码:

public class LazyStaticInnerClassSimpleSingletonTest {
    @Test
    public void test() throws Exception {
        Class <?> clazz = LazyStaticInnerClassSimpleSingleton.class;
        //获取所有无参构造器(包括私有)
        Constructor<?> constructor = clazz.getDeclaredConstructor(null);
        //强制访问
        constructor.setAccessible(true);
        //创建实例
        Object o1 = constructor.newInstance();
        Object o2 = constructor.newInstance();
        //比较是否是同一个实例对象
        System.out.println(o1);System.out.println(o2);System.out.println(o1==o2);
    }
}

运行结果: 可以发现,反射直接就绕过了单例类提供的唯一访问点。这就是反射破坏单例的事故现场。
在这里插入图片描述

解决办法:可以在无参构造方法里做个判断,抛异常。
在这里插入图片描述
再来看运行结果
是不是反射就创建不了对象了。但是,但是,本来很优雅的代码,结果你在构造方法中抛个异常,几个意思?感觉就很奇怪,这就不能忍了吧。且看下回分析。。。下边分析啊,新的写法又要来了,乃金刚不坏之身:序列化破坏不了它,反射也破坏不了它。
在这里插入图片描述

枚举式单例

堪称完美的单例啊,就下边一个缺点。。。
优点:优雅的代码,线程安全且避免了序列化和反射的破坏(因为反射是创建不了枚举对象的,直接会报错,待会看源码)。
缺点:类初始化时,就创建了这个枚举,在某些情况下可能造成内存浪费。

/**
 * 枚举式单例
 */
public enum  EnumSingleton {
    INSTANCE; //这个就是实例
    //定义一个属性,并提供get,set方法
    private String name;
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
    //全局访问点
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

测试代码:

public class EnumSingletonTest {
    @Test
    public void test(){
    	//创建两个实例对象
        EnumSingleton instance1 = EnumSingleton.getInstance();
        EnumSingleton instance2 = EnumSingleton.getInstance();
       	/**
         * 比较两个实例对象是否是同一个
         * 如果是同一个对象实例,那么返回true,并且用instance2可以获取到instance1设置的名字
        */
        System.out.println(instance1==instance2);
        instance1.setName("枚举单例");//用instance1 设置名字
        System.out.println(instance2.getName());//用instance2来获取名字
    }
}

运行结果:
在这里插入图片描述
聊一聊为什么枚举单例不会被反射破坏:
看一段源码:
找到Constructor这个类中的第416行,判断如果这个类是被ENUM(枚举)修饰的,那么就直接抛出Cannot reflectively create enum objects
靠,为啥人家抛异常就行,咱们在无参构造里抛个异常就不优雅了啊? 别问为啥,人家是官方,人家牛掰!
在这里插入图片描述

ThreadLocal单例

特点:保证线程内部的全局唯一,且天生线程安全。
注意:是线程内部全局唯一,也就是说,一个线程一个单例实例,线程之间是相互隔离的。

/**
 * ThreadLocal单例写法
 */
public class ThreadLocalSingleton {
    //私有化无参构造
    private ThreadLocalSingleton(){}
    //交给ThreadLocal创建实例
    private static final ThreadLocal<ThreadLocalSingleton> instance = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };
    //全局访问点
    private static ThreadLocalSingleton getInstance(){
        return instance.get();
    }
}

测试代码:

public class ThreadLocalSingletonTest {
    @Test
    public void test(){
        ThreadLocalSingleton mainInstance1 = ThreadLocalSingleton.getInstance();//主线程实例1
        ThreadLocalSingleton mainInstance2 = ThreadLocalSingleton.getInstance();//主线程实例2
        System.out.println("主线程两个实例:");
        System.out.println(mainInstance1);
        System.out.println(mainInstance2);

        new Thread(()->{
            ThreadLocalSingleton thread1Instance = ThreadLocalSingleton.getInstance();//thread1线程实例
            System.out.println("thread1线程实例:"+thread1Instance);
        }, "thread1").start();

        new Thread(()->{
            ThreadLocalSingleton thread2Instance = ThreadLocalSingleton.getInstance();//thread2线程实例
            System.out.println("thread2线程实例是否相同:"+thread2Instance);
        }, "thread2").start();
    }
}

运行结果:
可以看出来,主线程创建的两个实例是相同的,所以说:线程内部创建出来的实例是相同的
但是,每个线程创建出来的实例是不同的,所以说:一个线程一个单例实例,线程之间是相互隔离的。
在这里插入图片描述

单例模式总结

优点:
1.在内存中只有一个实例,减少内存开销
2.可以避免对资源的多重占用
3.设置全局访问点,严格控制访问

缺点:
1.没有接口,扩展困难,如果要扩展,只有修改代码,没有其他途径

重点:
1.私有化构造器
2.保证线程安全
3.延迟加载
4.防止被序列化和反序列化破坏
5.防止被反射破坏

<<上一篇:设计模式总览
>>下一篇:原型模式

发布了40 篇原创文章 · 获赞 59 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_45240169/article/details/104797982