首页 > 【手撕代码必问系列】你知道单例模式的单例有十种写法吗?
头像
码不停Ti2001
编辑于 2021-05-17 19:04
+ 关注

【手撕代码必问系列】你知道单例模式的单例有十种写法吗?

孔乙己是如此地让人快活,可是没有他,人们也照样过。有一天,大约是中秋的前两天,一个面试官说,孔乙己呢,他还欠我一个懒汉单例呢?我也才觉着他好久都没手撕代码了。一个经常抖机灵的说,他怎么会来?赔了个一干二净。面试官说哦?“他总是被面试八股,背昏了头,几天前竟然在牛客网里晒网易面试的面经,丁举人是学法律的,现在是资深老律师,自然起诉孔乙己,到最后孔乙己删了文章,赔了500袋茴香豆。”“后来呢?” “怕是退出了春招吧”

不抖机灵了
还是说回单例模式吧
我参加的春招的每场面试 但凡手撕代码了 都要手撕单例模式
对于从来没系统学过 多线程并发 和 23种常用设计模式的我 可谓是一下难死了
不过 单例模式也不是很难嘛 很容易就能从网上查到十种 单例模式的写法
看着 饿汉 懒汉也不是很难,但是 跟面试官一面对面 手撕的时候就紧张的 一行代码敲不出来
我把可能会 手撕的代码 传到了 github 上作为笔记 当然还没更新完
昨天一天 也就学了一个 单例模式而已
总结一下吧 其实也不是很难 只是自己太懒
https://github.com/HANXU2018/shousi

@[TOc]

单例模式是个啥

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

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

注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。

关键词:创建型模式、单一对象、访问唯一对象、无需实例化

菜鸟教程相关 文档

  • 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

  • 主要解决:一个全局使用的类频繁地创建与销毁。

  • 何时使用:当您想控制实例数目,节省系统资源的时候。

  • 如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

  • 关键代码:构造函数是私有的。

  • 应用实例:
    1、一个班级只有一个班主任。
    2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
    3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

  • 优点:
    1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
    2、避免对资源的多重占用(比如写文件操作)。
    缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

  • 使用场景:
    1、要求生产唯一序列号。
    2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
    3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

注意构造函数私有 我手撕代码的时候竟然忘记了,不应该呀 不应该~

单例模式的九种写法

笔记 整理了九种 单例模式写法
分为这

  1. 饿汉式单例
    • 【第一种】静态代码 生成单例
    • 【第二种】静态代码块生成单例
  2. 懒汉式单例
    • 【第三种】线程不安全单例
    • 【第四种】synchronized 懒汉单例
    • 【第五种】内部类懒汉单例
    • 【第六种】不允许反射破坏的内部类懒汉单例
    • 【第七种】不允许序列化破坏的内部类懒汉单例
    • DCL 双锁机制存在的问题
  3. 注册式单例
    • 【第八种】注册式spring容器单例
  4. ThreadLocal 式单例
    • 【第九种】ThreadLocal线程私有 式单例
  5. 枚举类 单例
    • 【第十种】JDK保护的枚举类单例

1. 饿汉式单例

【第一种】静态代码 生成单例

注意不要忘了 private HungrySingleton() { } 面试的时候我就忘了就很难受

package singleton.hungry;

public class HungrySingleton {
    private static final HungrySingleton hungrySigleton = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance(){
        return hungrySigleton;
    }
}

【第二种】静态代码块生成单例

在第一种的基础上 把原来的 初始化赋值 改成了 在 static 静态块里面操作

package singleton.hungry;

public class HungryStaticSingleton {
    private static final HungryStaticSingleton hungrySigleton;

    static {
        hungrySigleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton() {
    }

    public static HungryStaticSingleton getInstance(){
        return hungrySigleton;
    }
}

评价

  • 优点:创建对象时没有加任何的锁、执行效率比较高。
  • 缺点:也很明显,因为其在类加载的时候就初始化了,也就是说不管我们用或者不用都占着空间,如果项目中有大量单例对象,则可能会浪费大量内存空间。

2. 懒汉式单例

懒汉单例在 修改了 饿汉单例变成了 延时加载
需要的时候才初始化
这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。
因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

【第三种】线程不安全单例

如果两个线程同时走到 if(null == lazySingleton)
就会 创建多个实例 就破坏了单例模式

package singleton.lazy;

public class LazySingleton {
    private static LazySingleton lazySingleton = null;

    private LazySingleton() {
    }

    public static LazySingleton getInstance(){
        if(null == lazySingleton){//为空则说明第一次获取单例对象,进行初始化
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;//不为空则说明已经初始化了,直接返回
    }
}

【第四种】synchronized 懒汉单例

在第三种的基础上 加入
public synchronized static LazySyncSingleton getInstance(){
这样获取单例的时候就避免了 刚才说的那个 线程安全问题
但是让大家都卡在 getInstance 这里 效率也太低了吧
我们 能不能 只 锁 if(null == lazySingleton){ 这里的代码?

package singleton.lazy;

public class LazySyncSingleton {
    private static LazySyncSingleton lazySingleton = null;

    private LazySyncSingleton() {
    }

    public synchronized static LazySyncSingleton getInstance(){
        if(null == lazySingleton){
            lazySingleton = new LazySyncSingleton();
        }
        return lazySingleton;
    }
}

DCL 双锁机制存在的问题

这才是面试官想要看到的代码
必追问的内容 为什么两层 if 判断 null == lazySingleton

package singleton.lazy;

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazySingleton = null;

    private LazyDoubleCheckSingleton() {
    }

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

双重检查锁(double-checked locking) 除了两层if语句 还加入了 volatile 关键字
在 private volatile static LazyDoubleCheckSingleton lazySingleton = null; 上面

补充一下 volatile
volatile关键字为域变量的访问提供了一种免锁机制,
使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
因此每次使用该域就要重新计算,
而不是使用寄存器中的值。
需要注意的是,
volatile不会提供任何原子操作,
它也不能用来修饰final类型的变量。

关键信息提取一下 volatile 免锁 不缓存 不修饰 final 不是原子的 保证数据可见性 避免代码重排

第一使用 volatile 解决多线程下的可见性问题,

  • 因为我们的 getInstance 方法在判断 lazySingleton 是否为 null 时候并没有加锁,
  • 所以假如线程 t1 初始化过了对象,另外线程如 t2 是无法感知的,而加上了 volatile 就可以感知到。

第二把 synchronized 关键字移到了方法内部,尽可能缩小加锁的代码块,提升效率。

我以为已经很厉害了这样
结果还是有 bug 指令重排还存在着

new 对象的顺序

  1. 分配内存来创建对象,即:new。
  2. 创建一个对象 lazySingleton,此时 lazySingleton == null。
  3. 将 new 出来的对象赋值给 lazySingleton。
  • 实际运行的时候为了提升效率,
  • 这 3 步并不会按照实际顺序来运行的。
  • 那我们打个比方,假如有一个线程 t1 进入同步代码块正在创建对象,
  • 而此时执行了上面 3 个步骤中的后面 2 步,
  • 也就是说这时候 lazySingleton 已经不为 null 了,
  • 但是对象却并没有创建结束;
  • 此时又来了一个线程 t2 进入 getInstance 方法,
  • 这时候 if 条件肯定不成了,
  • 线程 t2 会直接返回,
  • 也就相当于返回了一个残缺不全的对象,
  • 这时候代码就会报错了

那还是看看 下面别的 单例模式方法吧

【第五种】内部类懒汉单例

package singleton.lazy;
public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton(){
    }
    public static final LazyInnerClassSingleton getInstance(){
        return InnerLazy.LAZY;
    }
    private static class InnerLazy{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

利用了内部类会等到外部调用时才会被初始化的特性,
用饿汉式单例的思想实现了懒汉式单例

饿汉单例思想 实现 懒汉式单例 感觉太厉害了 前人的智慧总是很厉害的。

【第六种】不允许反射破坏的内部类懒汉单例

前面说的很厉害
但是 Java 的 反射更厉害
单例 模式说破坏就破坏
constructor.setAccessible(true);
private 的限制说没就没

package singleton.lazy;

import java.lang.reflect.Constructor;

public class TestLazyInnerClassSingleton {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = LazyInnerClassSingleton.class;
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        Object o1 = constructor.newInstance();
        Object o2 = LazyInnerClassSingleton.getInstance();

        System.out.println(o1 == o2);
    }
}

上有政策下有对策
Java 既是矛又是盾 有枪也有保护罩
既然 破坏单例要 执行构造方法 constructor.newInstance();
那么我们 先把 构造方法拦截住

package singleton.lazy;

public class LazyInnerClassSingleton {

    private LazyInnerClassSingleton(){
        //防止反射破坏单例
         if(null != InnerLazy.LAZY){
           throw new RuntimeException("不允许通过反射类构造单例对象");
         }
    }
    public static final LazyInnerClassSingleton getInstance(){
        return InnerLazy.LAZY;
    }
    private static class InnerLazy{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

【第七种】不允许序列化破坏的内部类懒汉单例

除了反射会破坏 单例
序列化把java对象 保存到本地 再加载到内存 仍然会破坏单例模式

单例模式 用了 implements Serializable 实现了序列化 也把 破坏单例的 问题带来了

补充知识点 Extends 和 implements
Extends 可以理解为全盘继承了父类的功能。
implements 可以理解为为这个类附加一些额外的功能;
interface 定义一些方法,并没有实现,需要 implements 来实现才可用。
extend 可以继承一个接口,但仍是一个接口,也需要 implements 之后才可用。
对于 class 而言,Extends 用于(单)继承一个类(class),
而 implements 用于实现一个接口(interface)。

package singleton.lazy;

import java.io.Serializable;

public class LazyInnerClassSingleton implements Serializable {

    private LazyInnerClassSingleton(){
        //防止反射破坏单例
         if(null != InnerLazy.LAZY){
           throw new RuntimeException("不允许通过反射类构造单例对象");
         }
    }

    public static final LazyInnerClassSingleton getInstance(){
        return InnerLazy.LAZY;
    }

    private static class InnerLazy {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

我们用输入输出流来 破坏这个 单例

package singleton.lazy;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class TestLazyInnerClassSingleton2 {
    public static void main(String[] args) {
        //序列化攻击内部类式单例
        LazyInnerClassSingleton s1 = null;
        LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();

        FileOutputStream fos = null;

        try {
            fos = new FileOutputStream("LazyInnerClassSingleton.text");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.text");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (LazyInnerClassSingleton)ois.readObject();
            ois.close();

            System.out.println(s1 == s2);//输出:false

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

补充一下流的方法
java.io.ObjectOutputStream.flush() 方法刷新流。
这将写入所有缓冲的输出字节,并刷新到基础流。

这个问题 咋解决呢 ?
在单例类的代码加入 readResolve() 方法就行

package singleton.lazy;

import java.io.Serializable;

public class LazyInnerClassSingleton implements Serializable {

    private LazyInnerClassSingleton(){
        //防止反射破坏单例
         if(null != InnerLazy.LAZY){
           throw new RuntimeException("不允许通过反射类构造单例对象");
         }
    }

    public static final LazyInnerClassSingleton getInstance(){
        return InnerLazy.LAZY;
    }

    private static class InnerLazy {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }

    private Object readResolve(){
        return InnerLazy.LAZY;
    }
}

JDK 源码中在序列化的时候会检验一个类中是否存在一个 readResolve 方法,
如果存在,则会放弃通过序列化产生的对象,而返回原本的对象。

技术上 没有 银弹 啊

这种方式虽然保证了单例,
但是在校验是否存在 readResolve 方法前还是会产生一个对象,
只不过这个对象会在发现类中存在 readResolve 方法后丢掉,
然后返回原本的单例对象。
这种写法只是保证了结果的唯一,
但是过程中依然会被实例化多次,
假如创建对象的频率增大,
就意味着内存分配的开销也随之增大。

3. 注册式单例

spring 的 单例 bean 原来就是这么来的
技术都是 藕断丝连 循环交错的

【第八种】注册式spring容器单例

注册式单例就是将每一个实例都保存起来,
然后在需要使用的时候直接通过唯一的标识获取实例。

  1. private static Map<String,Object> ioc = new ConcurrentHashMap<>();//存储单例对象
  2. 用 getBean 获取 Bean 单例对象
    注入 的 代码
    obj = Class.forName(className).newInstance();
    ioc.put(className,obj);//将className作为唯一标识存入容器
    1. 单例直接从 map 里拿
      return ioc.get(className);//如果容器中已经存在了单例对象,则直接返回
package singleton.register;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ContainerSingleton {
    private ContainerSingleton(){
    }

    private static Map<String,Object> ioc = new ConcurrentHashMap<>();//存储单例对象

    public static Object getBean(String className){
        synchronized (ioc){
            if(!ioc.containsKey(className)){//如果容器中不存在当前对象
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className,obj);//将className作为唯一标识存入容器
                }catch (Exception e){
                    e.printStackTrace();
                }
                return obj;
            }
            return ioc.get(className);//如果容器中已经存在了单例对象,则直接返回
        }
    }
}

相关 测试代码
单例类

package singleton.register;

public class MyObject {
}

单元测试

package singleton.register;

public class TestContainerSingleton {
    public static void main(String[] args) {
        MyObject myObject1 = (MyObject) ContainerSingleton.getBean("singleton.register.MyObject");
        MyObject myObject2 = (MyObject) ContainerSingleton.getBean("singleton.register.MyObject");

        System.out.println(myObject1 == myObject2);//输出:true
    }
}

ioc 加入了 synchronized 关键字 如果不加入 默认是线程不安全的

4. ThreadLocal 式单例

ThreadLocal 式单例不能保证其创建的对象是全局唯一,
但是能保证在单个线程中是唯一的,
在单线程环境下线程天生安全。

【第九种】ThreadLocal线程私有 式单例

获得的单例是 private static final ThreadLocal<threadlocalsingleton> singleton</threadlocalsingleton>

package singleton.thread;

public class ThreadLocalSingleton {
    private ThreadLocalSingleton() {
    }

    private static final ThreadLocal<ThreadLocalSingleton> singleton =
            new ThreadLocal<ThreadLocalSingleton>() {
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };
    public static ThreadLocalSingleton getInstance(){
        return singleton.get();
    }
}

测试代码

package singleton.thread;

public class TestThreadLocalSingleton {

    public static void main(String[] args) {
        System.out.println(ThreadLocalSingleton.getInstance());//主线程输出
        System.out.println(ThreadLocalSingleton.getInstance());//主线程输出

        Thread t1 = new Thread(()-> {
            ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance();
            System.out.println(Thread.currentThread().getName() + ":" + singleton);//t1线程输出
        });
        t1.start();
    }
}

ThreadLocal 式示例仅对单线程是安全的

5. 枚举类 单例

【第十种】JDK保护的枚举类单例

单例类

package singleton.meiju;

public class MyObject {
}

枚举单例

package singleton.meiju;

public enum EnumSingleton {
    INSTANCE;

    private MyObject myObject;

    EnumSingleton() {
        this.myObject = new MyObject();
    }

    public Object getData() {
        return myObject;
    }

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

用枚举类型 来拿到 这个传统的单例对象

尝试反射破坏

package singleton.meiju;

import java.lang.reflect.Constructor;

public class TestEnumSingleton1 {

    public static void main(String[] args) throws Exception{
        //测试反射是否可以破坏枚举式单例
        Class clazz = EnumSingleton.class;
        Constructor c1 = clazz.getDeclaredConstructor();//无参构造器
        System.out.println(c1.newInstance());
    }
}

编译失败 没有无参构造方法

反编译后的枚举源码 发现无参构造方法是 假的
真实底层 是 string int 两个参数
在这里插入图片描述

修改后 反射还是被拒绝了

package DesignPattern.singletion.EnumSingleton;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class TestEnumSingleton1 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class clazzz = EnumSingleton.class;
       // Constructor c1 = clazzz.getDeclaredConstructor();
       Constructor c2 = clazzz.getDeclaredConstructor(String.class,int.class);
       c2.setAccessible(true);
       // JDK 底层在保护我们的枚举类不允许被反射创建
      //  System.out.println(c2.newInstance("测试",666));
//        System.out.println(c1.newInstance());

        /*
        @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }
         */

    }
}

通过查看 反射源码 newInstance 的 JDK 代码 发现 JDK 底层在保护我们的枚举类不允许被反射创建
在这里插入图片描述
那再 试试用 序列化破坏这个 枚举单例

package singleton.meiju;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class TestEnumSingleton2 {
    public static void main(String[] args) throws Exception{
        //测试序列化是否可以破坏枚举式单例
        EnumSingleton s1 = null;
        EnumSingleton s2 = EnumSingleton.getInstance();

        FileOutputStream fos = new FileOutputStream("EnumSingleton.text");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s2);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("EnumSingleton.text");
        ObjectInputStream ois = new ObjectInputStream(fis);
        s1 = (EnumSingleton)ois.readObject();
        ois.close();
        fis.close();
        System.out.println(s1.getData() == s2.getData());//true
    }
}

序列化也不能破坏我们单例。

这是因为,
在 Java 规范中规定了每个枚举类型及其定义的枚举变量在 JVM 中都必须是唯一的,
因此在枚举对象的序列化仅仅是将枚举对象的属性输出到结果中,
反序列化的时候则是通过 valueOf 方法来查找枚举对象。
枚举式单例之所以能成为最优雅的一种写法,原因就是 JDK 底层已经帮我们保证了不允许反射,也确保了序列化方式获得的对象仍然唯一。

总结下吧

手撕代码 看着代码行数也不多
门道倒是不少
特别是这个单例模式
我随便 在网上看看 竟然笔记里 整理了 零零碎碎 十种类型

真正掌握这些技术还是要靠平时的不断练习和总结
任重而道远
手撕代码的练习 代码 都放仓库里了以后也可以用来 翻一翻
感兴趣的可以 clone 下来看看
https://github.com/HANXU2018/shousi

全部评论

(5) 回帖
加载中...
话题 回帖

推荐话题

相关热帖

近期精华帖

热门推荐