单例模式不难,一篇搞懂

 单例模式

有些对象我们只需要一个,比如线程池、ServletContext、ApplicationContext、 Windows 中的回收站,此时我们便可以用到单例模式。

单例模式就是确保一个类在任何情况下只有一个实例,并提供一个全局访问点

1. 饿汉式单例

/**

* @author  java初学者组团学习737251827

* 饿汉式单例

*/

public class HungrySingleton {

    //类初始化的时候便进行对象实例化

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton() {

    }

    public static HungrySingleton getInstance() {

        return hungrySingleton;

    }

}

优点:

饿汉式单例是最简单的一种单例形式,它没有添加任何的锁,执行效率最高

线程安全

缺点:

某些情况下,造成内存浪费,因为对象未被使用的情况下就会被初始化,如果一个项目中的类多达上千个,在项目启动的时候便开始初始化可能并不是我们想要的。

2. 简单的懒汉式单例

想解决饿汉式单例一开始就会进行对象的初始化的问题,一个很自然的想法就是当用户调用getInstance方法的时候再进行实例的创建,修改代码如下:

/**

* @author

* 饿汉式单例

*/

public class LazySimpleSingleton {

    private static LazySimpleSingleton instance;

    private LazySimpleSingleton() {

    }

    public static LazySimpleSingleton getInstance() {

        // 如果实例不存在,则进行初始化

        if (instance == null) {

            instance = new LazySimpleSingleton();

        }

        return instance;

    }

}

上述代码在单线程下能够完美运行,但是在多线程下存在安全隐患。大家可以使用 IDEA 进行手动控制线程执行顺序来跟踪内存变化,下面我用图解的形式进行多线程下 3 种情形的说明。

情形 1:

每个线程依次执行 getInstance 方法,得到的结果正是我们所期望的

情形 2:

此种情形下,该种写法的单例模式会出现多线程安全问题,得到两个完全不同的对象

情形 3:

该种情形下,虽然表面上最终得到的对象是同一个,但是在底层上其实是生成了 2 个对象,只不过是后者覆盖了前者,不符合单例模式绝对只有一个实例的要求。

3. 升级的懒汉式单例

/**

* @author

* 饿汉式单例-同步锁

*/

public class LazySynchronizedSingleton {

    private static LazySynchronizedSingleton instance;

    private LazySynchronizedSingleton() {

    }


    //添加synchronized关键字

    public synchronized static LazySynchronizedSingleton getInstance() {

        if (instance == null) {

            instance = new LazySynchronizedSingleton();

        }

        return instance;

    }

}

升级之后的程序能完美地解决线程安全问题。

但是用synchronized加锁时,在线程数量较多的情况下,会导致大批线程阻塞,从而导致程序性能大幅下降

有没有一种形式,既能兼顾线程安全又能提升程序性能呢?有,这就是双重检查锁。

4. 双重检查锁

/**

* @author

* 双重检查锁

*/

public class LazyDoubleCheck {

    // 需要添加 volatile 关键字

    private volatile static LazyDoubleCheck instance;

    private LazyDoubleCheck() {

    }



    public static LazyDoubleCheck getInstance() {

        //一重检查:检查实例,如果不存在,进入同步区块

        if (instance == null) {

            synchronized (LazyDoubleCheck.class) {

                //双重检查:进入同步区块后,再检查一次,如果仍然是null,才创建实例

                if (instance == null) {

                    instance = new LazyDoubleCheck();

                }

            }

        }

        return instance;

    }

}

第一重检查是为了确认 instance 是否已经被实例化,如果是,则无需再进入同步代码块,直接返回实例化对象,否则进入同步代码块进行创建,避免每次都排队进入同步代码块影响效率;

第二重检查是真正与实例的创建相关,如果instance未被实例化,则在此过程中被实例化。

双重检查锁版本的单例模式需要使用到volatile关键字,本文不对volatile关键字进行深入分析,之后会单独开一篇文章进行解释

但是,使用synchronized关键总归是要上锁的,对程序性能还是存在影响,下面介绍一种利用 Java 本身语法特性来实现的一种单例写法。

5. 静态内部类实现单例

/**

* @author

* 静态内部类实现单例

*/

public class LazyStaticInnerClassSingleton {

    private LazyStaticInnerClassSingleton() {

    }

    public static final LazyStaticInnerClassSingleton getInstance() {

        return LazyHolder.LAZY;

    }


    // 静态内部类,未被使用时,是不会被加载的

    private static class LazyHolder {

        private static final LazyStaticInnerClassSingleton LAZY = new LazyStaticInnerClassSingleton();

    }

}

用静态内部类实现的单例本质上是一种懒汉式,因为在执行getInstance中的LazyHolder.LAZY语句之前,静态内部类并不会被加载。

这种方式既避免了饿汉式单例的内存浪费问题,又摆脱了synchronized关键字的性能问题,同时也不存在线程安全问题。

到此为止,我们介绍了 5 种单例写法(除去简单的懒汉式单例由于多线程问题无法用于生产中,其实只有 4 种),我们发现上述单例模式本质上都是将构造方法私有化,避免外部程序直接进行实例化来达到单例的目的。

那如果我们能够想办法获取到类的构造方法,或者将创建好的对象写入磁盘,然后多次加载到内存,是不是可以破坏上述所有的单例呢?

答案是肯定的,下面我们用反射和序列化两种方法亲自毁灭我们一手搭建的单例。

6. 反射破坏单例

/**

* @author

* 利用反射破坏单例

*/

public class SingletonBrokenByReflect {

    public static void main(String[] args) {

        try {

            Class<?> clazz = LazyStaticInnerClassSingleton.class;

            //通过反射弧获取类的私有构造方法

            Constructor c = clazz.getDeclaredConstructor(null);

            //强制访问

            c.setAccessible(true);

            Object obj1 = c.newInstance();

            Object obj2 = c.newInstance();

            //输出false

            System.out.println(obj1 == obj2);

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

}

如此,我们便使用反射破坏了单例。现在我们以静态内部类单例为例,解决这个问题。

我们在构造方法中添加一些限制,一旦检测到对象已经被实例化,但是构造方法仍然被调用时直接抛出异常。

/**

* @author

* 静态内部类实现单例

*/

public class LazyStaticInnerClassSingleton {

    private LazyStaticInnerClassSingleton() {

        if (LazyHolder.LAZY != null) {

            throw new RuntimeException("实例被重复创建");

        }

    }

    public static final LazyStaticInnerClassSingleton getInstance() {

        return LazyHolder.LAZY;

    }

    // 静态内部类,未被使用时,是不会被加载的

    private static class LazyHolder {

        private static final LazyStaticInnerClassSingleton LAZY = new LazyStaticInnerClassSingleton();

    }

}

7. 序列化破坏单例

单例对象创建好之后,有时需要将对象序列化然后写入磁盘,在需要时从磁盘中读取对象并加载至内存,反序列化后的对象会重新分配内存,如果序列化的目标对象恰好是单例对象,就会破坏单例模式。

/**

* @author

* 可序列化的单例

*/

public class SeriableSingleton implements Serializable {

    //类初始化的时候便进行对象实例化

    private static final SeriableSingleton hungrySingleton = new SeriableSingleton();

    private SeriableSingleton() {

    }

    public static SeriableSingleton getInstance() {

        return hungrySingleton;

    }

}

/**

* @author

* 序列化破坏单例

*/

public class SingletonBrokenBySerializing {

    public static void main(String[] args) {

        SeriableSingleton s1 = SeriableSingleton.getInstance();

        SeriableSingleton s2 = null;

        FileOutputStream fos = null;

        try {

            File file;

            fos = new FileOutputStream("SeriableSingleton.obj");

            OutputStream out;

            ObjectOutputStream oos = new ObjectOutputStream(fos);

            oos.writeObject(s1);

            oos.flush();

            oos.close();

            fos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");

            ObjectInputStream ois = new ObjectInputStream(fis);

            s2 = (SeriableSingleton) ois.readObject();

            ois.close();

            fis.close();

            //输出为false

            System.out.println(s1 == s2);

        } catch (Exception e) {

        }

    }

}

从运行结果上看,反序列化和手动创建出来的对象是不一致的,违反了单例模式的初衷。

那到底如何保证在序列化的情况下也能够实现单例模式呢,其实很简单,只需要增加一个 readResolve 方法即可。

public class SeriableSingleton implements Serializable {

    //类初始化的时候便进行对象实例化

    private static final SeriableSingleton hungrySingleton = new SeriableSingleton();

    private SeriableSingleton() {

    }

    public static SeriableSingleton getInstance() {

        return hungrySingleton;

    }

    //只需要添加这一个函数即可

    private Object readResolve() {

        return hungrySingleton;

    }

}

实现的原理涉及到ObjectInputStream的源码,不属于本文的研究重点,如果读者需要,我可以另开一篇来进行讲解。

8. 注册式单例模式

8.1 枚举式单例模式

很多博客和文章的实现方式如下(文件名:EnumSingleObject.java)

/*

* @author

* 枚举式单例1

*/

public class EnumSingleObject {

    private EnumSingleObject() {

    }

    enum SingletonEnum {

        INSTANCE;

        private EnumSingleObject instance;

        private SingletonEnum() {

            instance = new EnumSingleObject();

        }

        public EnumSingleObject getInstance() {

            return INSTANCE.instance;

        }

    }

    //对外暴露一个获取EnumSingleObject对象的静态方法

    public static EnumSingleObject getInstance() {

        return SingletonEnum.INSTANCE.getInstance();

    }

}

枚举式的写法为什么可以实现我们的单例模式呢,我们首先使用javac EnumSingleObject.java生成EnumSingleObject.class文件,用反编译工具Jad在.class 所在的目录下执行 jad EnumSingleObject.class命令,得到EnumSingleObject.jad文件,代码如下

static final class EnumSingleObject$SingletonEnum extends Enum {

    public static EnumSingleObject$SingletonEnum[] values() {

        return (EnumSingleObject$SingletonEnum[]) $VALUES.clone();

    }

    public static EnumSingleObject$SingletonEnum valueOf(String s) {

        return (EnumSingleObject$SingletonEnum) Enum.valueOf(com / chanmufeng / Singleton / registerSingleton / EnumSingleObject$SingletonEnum, s);

    }

    public EnumSingleObject getInstance() {

        return INSTANCE.instance;

    }

    public static final EnumSingleObject$SingletonEnum INSTANCE;

    private EnumSingleObject instance;

    private static final EnumSingleObject$SingletonEnum $VALUES[];

    // 该static代码块是枚举写法能够实现单例模式的关键

    static {

        INSTANCE = new EnumSingleObject$SingletonEnum("INSTANCE", 0);

        $VALUES = (new EnumSingleObject$SingletonEnum[]{

                INSTANCE

        });

    }

    private EnumSingleObject$SingletonEnum(String s, int i) {

        super(s, i);

        instance = new EnumSingleObject();

    }

}

其实,枚举式单例在静态代码块中就为INSTANCE进行了赋值,是一种饿汉式单例模式的体现,只不过这种饿汉式是 JDK 底层为我们做的操作,我们只是利用了 JDK 语法的特性罢了。

序列化能否破坏枚举式单例

//测试序列化能否破坏

public static void main(String[] args) {

        EnumSingleObject s1 = EnumSingleObject.getInstance();

        EnumSingleObject s2 = null;

        FileOutputStream fos = null;

        try {

            fos = new FileOutputStream("SeriableSingleton.obj");

            ObjectOutputStream oos = new ObjectOutputStream(fos);

            oos.writeObject(s1);

            oos.flush();

            oos.close();

            fos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");

            ObjectInputStream ois = new ObjectInputStream(fis);

            s2 = (EnumSingleObject) ois.readObject();

            ois.close();

            fis.close();

            //输出为false

            System.out.println(s1 == s2);

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

很遗憾,序列化依然会破坏枚举式单例EnumSingleObject

What???不是说枚举式单例非常的优雅吗?连Effective Java 都推荐使用吗?

别急,接下来我们观察另一种写法

/**

* @author

* 枚举式单例2

*/

public enum EnumSingleObject2 {

    INSTANCE;

    private Object data;

    public Object getData() {

        return data;

    }

    public void setData(Object data) {

        this.data = data;

    }

    public static EnumSingleObject2 getInstance() {

        return INSTANCE;

    }

}

我们再来进行序列化测试

public static void main(String[] args) {

        EnumSingleObject2 s1 = EnumSingleObject2.getInstance();

        s1.setData(new Object());

        EnumSingleObject2 s2 = null;

        FileOutputStream fos = null;

        try {

            fos = new FileOutputStream("SeriableSingleton.obj");

            ObjectOutputStream oos = new ObjectOutputStream(fos);

            oos.writeObject(s1);

            oos.flush();

            oos.close();

            fos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");

            ObjectInputStream ois = new ObjectInputStream(fis);

            s2 = (EnumSingleObject2) ois.readObject();

            ois.close();

            fis.close();

            //输出为true

            System.out.println(s1 == s2);

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

打印结果为true,说明枚举式单例 2 的写法可以防止序列化破坏。

而很多文章和博客用的往往是第 1 种写法,下面我们解释这两种写法的区别.

我们进入ObjectInputStream类的readObject()方法

public final Object readObject()

        throws IOException, ClassNotFoundException

    {

      ...

        try {

            Object obj = readObject0(false);

          ...

            return obj;

        } finally {

          ...

        }

    }

在readObject()方法中又调用了readObject0()方法

private Object readObject0(boolean unshared) throws IOException {

    ...

    //枚举式单例1的程序会进入到这里

    case TC_CLASS:

      return readClass(unshared);

    ...

    //枚举式单例2的程序会进入到这里

    case TC_ENUM:

      return checkResolve(readEnum(unshared));


}

我们先看一下readEnum()方法

private Enum<?> readEnum(boolean unshared) throws IOException {

        ...

        String name = readString(false);

        Enum<?> result = null;

        Class<?> cl = desc.forClass();

        if (cl != null) {

            try {

                @SuppressWarnings("unchecked")

                //!!!!这里是重点

                Enum<?> en = Enum.valueOf((Class)cl, name);

                result = en;

            } catch (IllegalArgumentException ex) {

              ...

            }


        }

        ...

        return result;

    }

到这里我们发现,枚举类型其实通过类名和类对象找到唯一一个枚举对象,因此,枚举对象不会被类加载器加载多次。

而readClass()并无此功能。

反射能否破坏枚举式单例

public static void main(String[] args) {

        try {

            Class<?> clazz = EnumSingleObject2.class;

            //通过反射获取类的私有构造方法

            Constructor c = clazz.getDeclaredConstructor(null);

            //强制访问

            c.setAccessible(true);

            Object obj1 = c.newInstance();

            Object obj2 = c.newInstance();

            //输出false

            System.out.println(obj1 == obj2);

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

运行结果如下

结果报了

java.lang.NoSuchMethodException异常,原因是java.lang.Enum中没有无参的构造方法,我们查看java.lang.Enum的源码,只有下面一个构造函数

protected Enum(String name, int ordinal) {

    this.name = name;

    this.ordinal = ordinal;

}

我们改变一下反射构建的方式

public static void main(String[] args) {

        try {

            Class<?> clazz = EnumSingleObject2.class;

            //通过反射获取类的私有构造方法

//            Constructor c = clazz.getDeclaredConstructor(null);

            Constructor c = clazz.getDeclaredConstructor(String.class, int.class);

            //强制访问

            c.setAccessible(true);

            Object obj1 = c.newInstance();

            Object obj2 = c.newInstance();

            //输出false

            System.out.println(obj1 == obj2);

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

运行结果如下

public T newInstance(Object ... initargs)

        throws InstantiationException, IllegalAccessException,

              IllegalArgumentException, InvocationTargetException

    {

        ...

        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()方法中做了强制性的判断,如果修饰符是Modifier.ENUM类型,则直接抛出异常。

最后介绍一种注册时单例的另一种写法:容器式单例

8.2 容器式单例模式

/**

* @author

* 容器式单例

*/

public class ContainerSingleton {

    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    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);


                } catch (Exception e) {

                    e.printStackTrace();

                }

                return obj;

            } else {

                return ioc.get(className);

            }

        }

    }

}

容器式单例适合用于实例非常多的情况,Spring 中就使用了该种单例模式。

总结

单例模式可以保证内存中任何情况下只有一个实例,是最简单的一种设计模式,实现起来也很简单,但是实现方式比较多,涉及到的小细节也比较多,在面试中是一个高频面试点。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容