设计模式之单例模式

一.定义

保证一个类仅有一个实例,并提供一个全局访问点

二.类型

创建型

三.适用场景

想确保任何情况下绝对只有一个实例,例如数据库连接池或者工厂等比较重的且线程安全的对象

四.优点

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

五.缺点

没有接口,扩展困难

六.重点

1.私有构造器
2.线程安全
3.延迟加载
4.序列化和反序列化安全
5.防御反射攻击

七.giao,单例模式的N种创建方式

1.懒汉式
package com.sunyard.factory.singleton;

public class LazySingleton {
    private static LazySingleton lazySingleton;
    private LazySingleton(){ } //构造函数私有化

    //加锁
    public static synchronized LazySingleton getInstance(){
        if(lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

注意:这种方式可以保证线程安全,但是锁太重,每次调用getInstance方法都会去获取synchronized锁,严重影响获取单例对象的效率,不推荐。

2.双重检测式
package com.sunyard.factory.singleton;

public class LazyDoubleCheckSingleton {
    private static volatile LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){ }
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton == null){
                    // 1.分配内存对象
                    // 2.初始化对象
                    // 3.设置LazyDoubleCheckSingleton指向刚分配的内存地址
                   //2.3顺序不固定
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

双重检测式创建单例模式,一定要记得用volatile关键字修饰单例对象的引用。当我们new一个对象时,表面上我们只调用的new 关键字,其实底层是分三步的:
(1)堆内存开辟一块空间
(2)初始化这个对象
(3)将对象的引用指向开辟的这块内存空间
其中(2),(3)两步是没有顺序性的,由于指令重排序的原因,很有可能是先执行步骤(3),再执行步骤(2)。那么在高并发的情况下,A线程先执行步骤(3)还没有来得及执行步骤(2),B线程一看对象的引用已经有了,认为对象已经创建完成了,直接返回了这个对象,就会导致不可预估的问题。
那么为什么volatile就可以解决这个问题呢?首先跟大家介绍一下volatile关键字的作用:
(1)内存可见
(2)防止指令重排
volatile产生内存屏障,禁止创建对象中的步骤(2)和(3)的重排序,使他们严格按照顺序执行,从而解决上述问题。
注意:volatile不管是实际运用中还是面试中都是高频知识点,后面我也会出一期关于volatile关键字的文章,这里就不多做阐述了,大家先记住结论就好。

3.静态内部类
package com.sunyard.factory.singleton;

public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton(){};
    private static class InnerClassHolder{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance(){
        return InnerClassHolder.staticInnerClassSingleton;
    }
}

利用JVM类加载的特性保证单例对象的创建,不需要加锁。这里提一个小知识点。
JVM在什么情况初始化一个类:
1)遇到字节码指令 new putstatic getstatic invokestatic
2)执行主类会被初始化(main方法那个类)
3)当初始化一个类得时候,父类未初始化,会优先触发父类的初始化
4)使用java.lang.reflect包的方法对类进行反射调用的时候,若类没有进行过初始化,则需要先触发初始化
我们这里就是利用了第一条,执行了getstatic字节码指令,开始初始化InnerClassHolder类,从而获取了单例对象。

4.饿汉式
package com.sunyard.factory.singleton;

public class HungrySingleton {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton(){}
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

饿汉式代码相对简单,也是利用了JVM类加载的特性。

5.枚举式
package com.sunyard.factory.singleton;

public enum SingletonEnum {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

枚举为什么可以保证对象是单例的呢?
这里我们可以采用反编译工具来把枚举类进行反编译,从而研究其天然单例性的原理。小伙伴们可以点击jad反编译工具进行下载,然后通过jad -sjava xxx.class命令将class文件反编译成java文件。

image.png

现在我们打开反编译后的java文件

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   SingletonEnum.java

package com.sunyard.factory.singleton;


public final class SingletonEnum extends Enum
{

    public static SingletonEnum[] values()
    {
        return (SingletonEnum[])$VALUES.clone();
    }

    public static SingletonEnum valueOf(String name)
    {
        return (SingletonEnum)Enum.valueOf(com/sunyard/factory/singleton/SingletonEnum, name);
    }

    private SingletonEnum(String s, int i)
    {
        super(s, i);
    }

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    public static final SingletonEnum INSTANCE;
    private String name;
    private static final SingletonEnum $VALUES[];

    static 
    {
        INSTANCE = new SingletonEnum("INSTANCE", 0);
        $VALUES = (new SingletonEnum[] {
            INSTANCE
        });
    }
}

通过反编译后的代码我们就可以看到枚举类型是static final修饰的(意味着不可变),在静态代码块中创建对象,所以枚举可以认为是饿汉式的一种变式。

八.序列化+反序列化破坏单例的解决方案

1.单例模式真的单例吗?
我们都知道当我们需要任何情况下绝对只有一个对象时会采用单例模式进行创建对象,但是单例真的单例吗?请看下面的例子:
1.实现了Serializable的饿汉式单例

package com.sunyard.factory.singleton;

import java.io.Serializable;

public class HungrySingleton implements Serializable {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton(){}
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

2.通过序列化和反序列化测试

package com.sunyard.factory.singleton;

import java.io.*;

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
        HungrySingleton hungrySingleton = HungrySingleton.getInstance();
        oos.writeObject(hungrySingleton);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        HungrySingleton newSingleton = (HungrySingleton) objectInputStream.readObject();
        System.out.println(newSingleton == hungrySingleton);
        //  false
    }
}

通过上面序列化和反序列化测试,发现我们创建了一个新的对象。
2.为什么通过序列化和反序列化可以破坏单例
查看ObjectInputStream中readObject()源码

image.png

image.png

image.png

ObjectInputStream的readObject()方法是调用反射创建对象的自然就会破坏单例结构
注意:枚举式单例不会被序列化和反序列化破坏,有兴趣的小伙伴可以看下readObject()方法对枚举类型的处理

image.png

2.如何解决
通过源码可以看到readObject()会判断是否有ReadResolveMethod,如果有就会执行该方法。

image.png

那么这个ReadResolveMethod到底是什么方法呢?
image.png

相信现在小伙伴们一定恍然大悟,我们只要在单例模式里新增下面的方法,就可以保证序列化和反序列化后还是同一个对象。但是这里我们要注意虽然该方法返回了原来的对象,但是也通过反射生成了新的对象,只是新对象没有被使用)

package com.sunyard.factory.singleton;

import java.io.Serializable;

public class HungrySingleton implements Serializable {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton(){}
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
//新增 readResolve方法
    public Object readResolve(){
        return hungrySingleton;
    }
}

测试

package com.sunyard.factory.singleton;

import java.io.*;

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
        HungrySingleton hungrySingleton = HungrySingleton.getInstance();
        oos.writeObject(hungrySingleton);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        HungrySingleton newSingleton = (HungrySingleton) objectInputStream.readObject();
        System.out.println(newSingleton == hungrySingleton);
        //  true
    }
}

九.反射攻击破坏单例模式解决方案

1.针对利用类加载时机创建单例模式的方案(饿汉式,静态内部类方式),可以在构造方法中判断对象是否已经创建,若创建就抛出异常,告诉调用者不能通过反射进行创建对象。
2.针对懒汉式 -> 无解
3.枚举类型 ->天然防御攻击(最佳方案)

十.克隆(原型模式)破坏单例模式的解决方案

1.不实现cloneable接口
2.重写Object类中的Clone方法返回getInstance();

小知识点:clone是浅拷贝,在使用clone构造对象的时候,一定要对深拷贝浅拷贝有一个深入的认识,不然可能就会出现难以预估的问题。后面有机会再写一篇文章来和大家一起探讨下深拷贝和浅拷贝相关的话题。

十一.JDK源码单例模式的应用

1.Runtime.getRuntime()
image.png

相信聪明的你一定可以看出来这是哪种单例模式了 ^ ^

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

推荐阅读更多精彩内容

  • 概述 单例模式是应用最广的模式之一,在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要...
    刘涤生阅读 1,020评论 0 5
  • 一.什么是单例模式 单例模式的定义:确保一个类只有一个实例,并提供一个访问他的全局访问点。单例模式是几个设计模式中...
    Geeks_Liu阅读 2,224评论 0 10
  • 1、嘻哈说 首先,请您欣赏单例模式的原创歌曲。 试听请点击这里 闲来无事听听曲,知识已填脑中去; 学习复习新方式,...
    番茄课堂_懒人阅读 436评论 0 0
  • 江浙沪包邮区、长三角经济圈,说起来实在是高大上,可是每日三餐,日出而落、日落而息,是人跟人都一样的,和地区...
    楼楼_a0b3阅读 465评论 0 1
  • 未完成事项 1、Alicia金额核对 2、审核应收 多完成事项 1、完成了所有产后流程 原因 1、财务今天没上班,...
    沈祁洪阅读 95评论 0 0