java设计模式之单例模式

什么是单例模式?

单例模式:是指在内存中有且只会创建一次对象的创建型-设计模式,在程序多次使用同一个对象作用相同的时候,为了防止频繁创建和消费对象,单例模式可以让程序中只创建一个对象

单例模式的优点:

在内存中只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁对象,避免对资源的占用和浪费

单例模式的缺点:

1、由于单例模式中没有抽象层,因此单例类的扩展有很大的困难

2、单例类职责过重,在一定程度违背了 “单一职责原则”

3、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池设计为单例类,会导致共享连接池对象过多出现连接池溢出,如果实例出的对象长时间不被利用,系统会认为是垃圾进行回收,这将导致对象的状态丢失

单例模式的适用场景:

1、需要频繁实例化然后销毁的对象

2、创建对象时耗时过多或者耗资源过多,但又经常用到的对象

3、一些开发工具类

4、一些大量使用的配置文件对象

单例模式的特点:

1、单例模式的构造方法是私有的

2、单例对象必须由单例类自行创建

3、单例类对外提供统一获取实例的静态方法

单例模式的实现;

1、饿汉式单例(线程安全)

饿汉式是最简单的单例模式写法,类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以保证了线程安全,但是如果长时间没使用这个方法,会浪费系统资源,所以不建议使用。

/**
 * @Author LvHui
 * @Date 13:28 2022/7/14
 * @Description 单例模式的实现之饿汉式
 * 优点:线程安全,调用效率高,
 * 缺点:不能延迟加载,如果不使用则会浪费系统资源
 * 测试后结论;因为是类初始化的时候加载的静态变量,所属在使用的时候不会出现线程安全的问题,又因为获取实例方法没有同步锁所以效率会高
 **/
public class HungerType {

    //类初始化时,立即加载这个对象
    private static final HungerType hungerType = new HungerType();

    //私有化构造器
    private HungerType() {
    }

    //提供统一的访问方法
    public static HungerType getInstance() {
        return hungerType;
    }
}
2、懒汉式单例(线程不安全)

该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例。代码如下:

/**
懒汉式单例-线程不安全
**/
public class LazyType {

    private static LazyType lazyType = null;

    private LazyType() {
    }

    public static LazyType getInstance() {
      
        if (lazyType == null) {

            lazyType = new LazyType();

        }
        return lazyType;
    }
}


这种方式实现的单例模式有一个问题,如果多个线程同时调用getInstance()方法时,由于没有锁机制,会导致实例化两个实例的情况,因此在多线程环境下是不安全

3、懒汉式单例(线程安全)
/**
懒汉式单例-线程安全
**/
public class LazyType {

    private static LazyType lazyType = null;

    private LazyType() {
    }
    //通过增加同步锁来解决线程安全问题
    public synchronized   static LazyType getInstance() {
      
        if (lazyType == null) {

            lazyType = new LazyType();

        }
        return lazyType;
    }
}

如上代码所示,getInstance()方法添加了同步锁,虽然解决了线程安全问题,但却也带来了另外一个问题,就是每次获取实例都需要加锁和释放锁,效率较低,继续往下优化代码

4、懒汉式单例(双重检测DCL)

经过思考我们判断只有第一次获取实例的时候才会有线程安全问题,所以我们考虑使用懒加载方式,只需要第一次的时候进行加锁,后续的操作直接返回实例,但如果只有一层判断后再加锁会有问题,如第一次两个线程同时满足第一个判空条件,然后等待获取锁,破坏了单例唯一性条件,所以需要进行二次校验判空语句。


/**
懒汉式单例-双重检测(DCL即 double-checked locking)
**/
public class LazyType {
    //增加volatile关键字
    private static volatile  LazyType lazyType = null;

    private LazyType() {
    }
   //采用双层检测方式获取实例对象
    public static LazyType getInstance() {      
        if (lazyType == null) {
          synchronized (LazyType .class){
            if (lazyType == null) {
            lazyType = new LazyType();
          }
          }
       }
        return lazyType;
    }
}
但这样就没问题了吗?

由于java内存模型允许 “无序写入”,有些时候编译器会因为性能问题,把代码进行指令重排序,可能顺序发生颠倒一般而言初始化操作并不是一个原子操作,而是分为三步

1、在堆内存中开辟对象所需要的空间,分配地址
2、根据类加载器的顺序进行初始化对象执行构造方法
3、把堆分配的地址返回给栈中的引用变量

在成员变量lazyType上修饰了volatile关键字,该关键字是为了保证可见性,防止指令重排带来的顺序性问题,因为初始化对象。不是一个原子操作,jvm可能为了优化代码会进行指令重排,导致执行了顺序发生改变,如执行流程原本是1->2->3,但可能由于指令重排改为1->3->2,这就会导致如果执行到第二步操作,另一个线程获取实例,判断不为空,获取到尚未构造完成的对象,所以为了避免这个问题,则需要用到volatile关键字来防止指令重排保证操作的有序性。

5、静态内部类单例(线程安全,懒加载)

静态内部类单例模式也比较推荐的一种单例实现,因为相比懒汉式,它用更少的代码量达到了延迟加载的目的,这种方式不仅能够保证线程安全,也能保证单例对象的唯一性,同时也延迟实例化,是一种非常推荐的方式。

那么静态内部类是如何实现线程安全呢?首先,我们先了解一下类的加载时机

java虚拟机在仅有5种情况下会对类进行初始化
1、遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时读取、或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化
3、初始化一个类的时候发现有父类还未进行初始化,则先初始化父类
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5.当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。

当getInstance()方法被调用时,静态内部类才在StatisticsInnerClassType 的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,这点同饿汉模式。那么INSTANCE在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。

故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

此处摘自 :深入理解单例模式

/**
 * @Author LvHui
 * @Date 23:37 2022/7/16
 * @Description 单例模式-静态内部类方式 懒加载
 * 静态内部类能实现单例模式有以下两点
 * 1、jvm在加载类的时候不会加载静态内部类,在使用到静态内部类的时候才会对静态内部类进行加载,和懒汉模式一样,节省系统资源
 * 2、jvm底层保证类加载的安全,即使在高并发情况下,类的加载只有一次,就保证了创建单例时并发安全性
 **/
public class StatisticsInnerClassType {

    private StatisticsInnerClassType() {
    }

    public static StatisticsInnerClassType getInstance() {
        return InnerClass.HOLDER;
    }
    /**
     * 静态内部类与外部类的实例没有绑定关系,而且只有被调用时才会
     * 加载,从而实现了延迟加载
     */
    public static class InnerClass {
        /**
         * 静态初始化器,由JVM来保证线程安全
         */
        private static final StatisticsInnerClassType HOLDER = new StatisticsInnerClassType();
    }
}
6、枚举单例(线程安全,天生防止反射)

以上静态内部类的方式虽然解决了线程安全和延迟加载的问题,但是,还是存在一些如下问题
1、反射可以获取到构造函数并设置为可访问,并生成新的对象。
2、clone的深克隆会生成新的对象
3、反序列化生成新的对象

由于单例模式的枚举实现代码比较简单,而且又可以利用枚举的特性来解决线程安全和单一实例的问题,还可以防止反射和反序列化对单例的破坏,通过实现Serializable接口增加readResolve方法来防止反序列化对单例的破坏,具体参考以下代码

/**
 * @Author LvHui
 * @Date 13:28 2022/7/14
 * @Description 单例模式的实现之枚举
 * 由于单例模式的枚举实现代码比较简单,而且又可以利用枚举的特性来解决线程安全和单一实例的问题,还可以防止反射和反序列化对单例的破坏
 * 通过实现Serializable接口增加readResolve方法来防止反序列化对单例的破坏
 **/
public class EnumSingle implements Serializable {

    private static final long serialVersionUID = 1L;

    private EnumSingle() {
    }

    public enum SingletonEnum {
        SINGLETON;
        private EnumSingle enumSingle = null;

        SingletonEnum() {
            enumSingle = new EnumSingle();
        }

        public EnumSingle getInstance() {
            return enumSingle;
        }

    }

    //此方法来防止反序列化对单例的破坏,具体可参考ObjectInputStream的readObject()方法
    private Object readResolve() {
        return SingletonEnum.SINGLETON.getInstance();
    }
}
为什么枚举类型是线程安全的?

通过反编译枚举的class文件发现属性被static final修饰,根据类加载机制,static类型的属性和方法会在类加载的过程中初始化,当第一次调用初始化类,因为初始化加载类的时候classload方法是加锁保证线程安全的,所以enum是线程安全的。

微信截图_20220719225529.png
为什么枚举类型反序列化也不会创建新的实例?

枚举类型在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找values()数组种的枚举对象同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,而普通的类反序列化是通过反射重新创建对象,破坏了单例的唯一原则。所以枚举类型反序列化也不会创建新的实例

单例的模式每种各有优缺点,但推荐的还是静态内部类和枚举的单例模式。

本人水平有限,如果有地方存在一些错误和不足,麻烦各位提醒,这边我会及时修改错误的地方避免影响大家的理解。

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

推荐阅读更多精彩内容