zoukankan      html  css  js  c++  java
  • java基础之----23种设计模式(单例模式)

    概述

      提到单例模式,我们并不陌生,而且面试中也常常会问单例模式相关的问题,接下来就谈谈单例模式,这篇文章会回答如下几个问题:

    1. 什么是单例模式?
    2. 单例模式有几种实现方式,以及各种实现方式的优劣?
    3. 单例模式有什么用?

    什么是单例模式

      保证一个类仅有一个实例,并提供一个访问它的全局访问点。这是《设计模式》(艾迪生维斯理, 1994)中的定义。

    单例模式有几种实现方式

      先说答案,有三种,基本上我们只知道两种,就是懒汉式和饿汉式,其实还有第三种,通过静态内部类实现单例模式,下面就说说这三种实现方式。

    饿汉式

      

    package com.gxl.demo.DesignPattern.singletonpattern;
    
    /**
     * Description: 饿汉式单例模式
     */
    public class EagerSingleton {
        //为什么EagerSingleton要是static呢?因为声明成static就是当类被加载的时候就会执行new EagerSingleton(),
        // 无论getInstance方法有没有被调用,这也是饿汉式这个名称由来,就是说,无论你用不用,我都会实例化。为什么是final类型呢?
        // 因为声明成final类型就是当类被加载实例化之后,
        // 这个instance的值就不允许修改了,就是说只能被实例化一次
        private final static EagerSingleton instance = new EagerSingleton();
        //私有化构造方法,不允许外部应用通过new来创建对象
        private EagerSingleton(){}
        //静态方法,提供给外部应用获取单例对象
        public static EagerSingleton getInstance(){
            return instance;
        }
    }
    
    class Test{
        public static void main(String[] args) {
            EagerSingleton eagerSingleton1 = EagerSingleton.getInstance();
            EagerSingleton eagerSingleton2 = EagerSingleton.getInstance();
            if (eagerSingleton1 == eagerSingleton2){
                System.out.println("是单例模式,保证了单例对象的唯一性");
            }
        }
    }

    饿汉式总结

    饿汉式在没有被使用的情况下就把单例对象创建出来,占用内存,并不是一种很好的方式。

    懒汉式

    /**
     * Description: 懒汉式单例模式
     */
    public class LazySingleton {
    
        private static LazySingleton instance = null;
        //私有防止外部应用new新的实例
        private LazySingleton(){}
        //双重检验锁定,先判断instance是否为null而不是先锁定的好处是如果单例已经创建就不用在锁定
        //提升效率,锁定之后再次判断instance是否为null的目的是为了防止多个线程同时通过了第一个判断
        //然后创建多个对象
        public static LazySingleton getInstance(){
            if (instance == null){
                synchronized (LazySingleton.class){
                    if (instance == null){
                        instance = new LazySingleton();
                    }
                }
            }
            return instance;
        }
    }

    懒汉式总结

      为什么叫做懒汉式呢?因为只有当调用getInstance方法才会实例化,这就是一种懒加载。懒汉式解决了饿汉式占用资源的问题,但是又有一个新问题,就是使用synchronized效率低下。那有没有一种完美的方式,既不占用内存,效率又很高的方式呢?就是下面要介绍的。

    IoDH(Initialization Demand Holder (IoDH))

    package com.gxl.demo.DesignPattern.singletonpattern;
    
    /**
     * Description:IoDH
     */
    public class IoDHSingleton {
        //私有化构造方法,防止外部应用new新实例
        private IoDHSingleton(){}
        //不会初始化内部类,如果这个不是内部类就会被初始化,因为类中存在静态变量,并且静态变量被初始化赋值
        private static class HodlerClass{
            private final static IoDHSingleton instance = new IoDHSingleton();
        }
        //只有这个方法被调用,上面的静态内部类才会被初始化
        public static IoDHSingleton getInstance(){
            return HodlerClass.instance;
        }
        
        public static void main(String[] args) {
            IoDHSingleton ioDHSingleton1 = IoDHSingleton.getInstance();
            IoDHSingleton ioDHSingleton2 = IoDHSingleton.getInstance();
            if (ioDHSingleton1 == ioDHSingleton2){
                System.out.println("是单例模式");
            }
        }
    }

    IoDH总结

    其实上面的代码有两个疑问,上面我说过,IoDH完美的解决了延迟加载,而且不需要使用锁,第一个问题,为什么通过静态内部类可以实现延迟加载?第二个问题,不加锁如何保证线程安全?其实第二个问题在饿汉式中也存在这个问题,那下面来回答这两个问题。

    第一个问题,为什么通过静态内部类可以实现延迟加载?

    要解决第一个问题,需要先明白类什么时候会被加载。

    注:以下内容参考这篇博客:深入理解单例模式:静态内部类单例原理

    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种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。

    可以看出,静态内部类不属于上面5条的任意一条,所以不会被初始化,这里要区分一下类的初始化和类的实例化:

    类的实例化是指创建一个类的实例(对象)的过程;
    类的初始化是指为类中各个类成员(被static修饰的成员变量)赋初始值的过程,是类生命周期中的一个阶段。

    第二个问题,不加锁如何保证线程安全?

    要回答这个问题,需要明白另一个问题,就是类初始化时,jvm是否可以保证其本身是线程安全的?

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

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

  • 相关阅读:
    JSON文件和Json对象
    如何判断一个数为几进制
    热更新
    UIPageView
    quick-cocos2d中自定义按钮(BUTTON)
    lua与oc交互
    Test1
    Visual Studio 附加到进程调试
    C# 判断一个单链表是否有环及环长和环的入口点
    Asp.Net Forms获取UEeditor内容
  • 原文地址:https://www.cnblogs.com/gunduzi/p/12202848.html
Copyright © 2011-2022 走看看