设计模式---单例模式应用场景,案例介绍,原理分析

单例模式应用场景:

  1. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~

  2. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

  3. 网站的计数器,一般也是采用单例模式实现,否则难以同步。

  4. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。

  5. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。

  6. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。

  7. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。

  8. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。

  9. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.

总结以上,不难看出:

单例模式应用的场景一般发现在以下条件下:

(1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。

(2)控制资源的情况下,方便资源之间的互相通信。如线程池等。

实现单例模式的方式有饿汉式,懒汉式,静态(static)内部类,双重检查锁,等等方式来实现。
首先这里我需要了解饿汉式:

public class HungrySingle {

    private HungrySingle(){

    }

    private final static HungrySingle HUNGRY_SINGLE = new HungrySingle();

    public static HungrySingle getHungrySingle(){
        return HUNGRY_SINGLE;
    }
    /*

    * */
}

饿汉式的优点和缺点:

  • 饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。
  • 从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。
  • 饿汉式的缺点在一定的场景下可能会浪费空间,因为一旦初始化就会创建。那么我们想到想用的时候再创建这个对象.

现在我需要了解懒汉式:

public class LayzSingle {

//    DCL懒汉式
    //第一步构造器私有化
    private LayzSingle(){
        System.out.println(Thread.currentThread().getName()+"OK");
    }

    private static LayzSingle layzSingle;

    public static LayzSingle getInstance(){
        if (layzSingle == null){
            layzSingle = new LayzSingle();
        }
        return layzSingle;
    }

    //单线程下可以使用,如果是并发的情况是不能使用这个方法的
    public static void main(String[] args) {
        for (int i = 0; i <10 ; i++) {
            new Thread(()->{
                LayzSingle.getInstance();
            }).start();
        }
    }
}
  • 懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。

双重检查锁:

public class LayzSingleTwo {

//    DCL懒汉式
    //第一步构造器私有化
    private LayzSingleTwo(){
        System.out.println(Thread.currentThread().getName()+"OK");
    }
//volatile 这里保证原子性操作
    private volatile static LayzSingleTwo layzSingle;

    public static LayzSingleTwo getInstance(){
        //加锁 双重检测 懒汉式单例 DCL懒汉式是有一个问题的.
        if (layzSingle ==null)
            //锁class的话只有一个
            synchronized (LayzSingle.class){
                if (layzSingle == null){
                    layzSingle = new LayzSingleTwo();//这不是原子性操作.
                    /*
                    * 第一步 分配内存空间
                    * 第二部 执行构造方法 初始化对象
                    * 第三步 把对象指向分配的内存空间
                    *
                    * 有可能发生指令从排的情况.
                    * 可能第一个线程进入如果先分配内存空间,在把空对象指向这个空间,在进行初始化对象.
                    * 1,3,2。
                    * 那么当第一个线程把这个没有初始化的对象指向这个空间后,还没有来得及执行初始化对象。这时候layzSingle
                    * 就不会为null了,但是第二个线程进入判断layzSingle==null的时候,是为false那么就会直接返回layzSingle。
                    * 这就出问题了。layzSingle还没有完成构造。所以我们要加上volatile,双重指令锁,加原子性操作。
                    * */
                }
            }
        return layzSingle;
    }

    //
    public static void main(String[] args) {
        for (int i = 0; i <10 ; i++) {
            new Thread(()->{
                LayzSingleTwo.getInstance();
            }).start();
        }
    }
}

单例类中增加一个静态(static)内部类

public class Singleton{

       private  Singleton(){}

      private static class HolderClass{
			 private final static Singleton  instance = new Singleton();
		}

	  public static Singleton getInstance(){
			return HolderClass.instance;
		}
		
	  public  static void main(String args[]){
			Singleton  s1 = Singleton.getInstance();
			Singleton  s2  = Singleton.getInstance();
			 System.out.println(s1==s2);
		}
}
  • 由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。

以上是实现单例模式的一些方式。
但是以上的这些方式是不安全的,如果使用反射和序列化是可以破坏单例模式的。做一下的测试就可以看出:

 public class SingleReflection {

   
    //DCL懒汉式
    //第一步构造器私有化
//    private SingleReflection(){
        //这是解决反射破坏单例模式方式一的解决办法.不是根本的解决方法.方式一解决:
//        synchronized (SingleReflection.class) {
//            if (singleReflection != null) {
//                throw new RuntimeException("不要用反射破坏单例模式");
//            }
//        }
//    }

//这是解决反射破坏单例模式二的解决拌饭.不是根本的解决方法.方式二解决:
 private static boolean miyao = false;
    //DCL懒汉式
    //第一步构造器私有化
    private SingleReflection(){
        synchronized (SingleReflection.class) {
            if (miyao == false){
                miyao = true;
            }else {
                throw new RuntimeException("不要用反射破坏单例模式");
            }
        }
    }

    private volatile static SingleReflection singleReflection;

    public static SingleReflection getInstance(){
        //加锁 双重检测 懒汉式单例 DCL懒汉式是有一个问题的.
        if (singleReflection ==null)
            //锁class的话只有一个
            synchronized (SingleReflection.class){
                if (singleReflection == null){
                    singleReflection = new SingleReflection();//这不是原子性操作.
                    /*
                     * 第一步 分配内存空间
                     * 第二部 执行构造方法 初始化对象
                     * 第三步 把对象指向分配的内存空间
                     *
                     * 有可能发生指令从排的情况.
                     * 可能第一个线程进入如果先分配内存空间,在把空对象指向这个空间,在进行初始化对象.
                     * 1,3,2。
                     * 那么当第一个线程把这个没有初始化的对象指向这个空间后,还没有来得及执行初始化对象。这时候layzSingle
                     * 就不会为null了,但是第二个线程进入判断layzSingle==null的时候,是为false那么就会直接返回layzSingle。
                     * 这就出问题了。layzSingle还没有完成构造。所以我们要加上volatile,双重指令锁,加原子性操作。
                     * */
                }
            }
        return singleReflection;
    }

    //
    public static void main(String[] args) throws Exception {
        //反射破解单例模式方式一的问题
//        //反射可以破坏单例模式
//        SingleReflection instance = SingleReflection.getInstance();
//        //无视构造器
//        Constructor<SingleReflection> declaredConstructor = SingleReflection.class.getDeclaredConstructor(null);
//        declaredConstructor.setAccessible(true);
//        SingleReflection singleReflection2 = declaredConstructor.newInstance();
//        System.out.println(instance);
//        System.out.println(singleReflection);

        //反射破解单例模式方式二的问题:破坏方式一的解决方法
        //无视构造器
//        Constructor<SingleReflection> declaredConstructor = SingleReflection.class.getDeclaredConstructor(null);
//        declaredConstructor.setAccessible(true);
//        SingleReflection singleReflection = declaredConstructor.newInstance();
//        SingleReflection singleReflection2 = declaredConstructor.newInstance();
//        System.out.println(singleReflection2);
//        System.out.println(singleReflection);

        //反射破解单例模式方式三继续破坏方式二的解决方法:
        Field miyao = SingleReflection.class.getDeclaredField("miyao");
        miyao.setAccessible(true);
        Constructor<SingleReflection> declaredConstructor = SingleReflection.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        SingleReflection singleReflection = declaredConstructor.newInstance();

        miyao.set(singleReflection,false);

        SingleReflection singleReflection2 = declaredConstructor.newInstance();

        System.out.println(singleReflection2);
        System.out.println(singleReflection);
    }
}

如此以上问题我们需要通过枚举的方式来解决反射破坏单例模式的问题:
首先我们看枚举newInstance()方法的源码:
//查看反射调用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);
            }
        }
        //这里加了一个判断如果是ENUM的话就会抛出异常。
        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;
    }

以下是用枚举实现单例模式:

public enum EnumSingleton {
    INSTANCE;// 枚举里的属性相当于Singleton的实例
    private SingletonClassT instance;
    private EnumSingleton() {
        instance = new SingletonClassT();
    }
    public SingletonClassT getInstance() {
        return instance;
    }

    class SingletonClassT {

        private SingletonClassT(){
            System.out.println("这里只是用来证明SingletonClassT被实例化了。");
        }
    }
}

class TestDemoT{
    public static void main(String[] args) throws Exception {
        EnumSingleton.SingletonClassT instance1 = EnumSingleton.INSTANCE.getInstance();
        EnumSingleton.SingletonClassT instance2 = EnumSingleton.INSTANCE.getInstance();
        System.out.println(instance1 == instance2);

//        Constructor<EnumSingleton.SingletonClassT> declaredConstructor = EnumSingleton.SingletonClassT.class.getDeclaredConstructor(null);
//        declaredConstructor.setAccessible(true);
//        EnumSingleton.SingletonClassT singletonClassT = declaredConstructor.newInstance();
//        EnumSingleton.SingletonClassT singletonClassT2 = declaredConstructor.newInstance();
//        System.out.println(singletonClassT);
//        System.out.println(singletonClassT2);


        //这段代码我是通过用枚举类来测试的用反射来破解单例结果会报错:
//        try {
//			//下面两句代码是可以成功的因为这里没有用反射来newInstance,而是直接用的getInstance()所以没有问题。
//            EnumSingleton.SingletonClassT instance3 = EnumSingleton.INSTANCE.getInstance();
//            System.out.println("instance3"+instance3);
//        //这是我们运用反编译后看到的源码情况,如果不看源码IDEA反编译出来的会是一个空参的构造函数,但是用专业一点的反编译软件会是有参的构造函数
//            Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
//            declaredConstructor.setAccessible(true);
//            EnumSingleton enumSingleton = declaredConstructor.newInstance();
//            EnumSingleton singletonClass = declaredConstructor.newInstance();
//            System.out.println(enumSingleton);
//            System.out.println(singletonClass);
//            System.out.println(enumSingleton == singletonClass);
//        }catch (Exception e){
//            e.printStackTrace();
//        }
//以下是报的错误:
//		java.lang.IllegalArgumentException: Cannot reflectively create enum objects
//			at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
//			at cn.com.skyvis.Mystudy.correctSingle.TestDemoT.main(EnumSingleton.java:45)

        //如果用SingletonClassT类型来破解话,你看枚举类的源码会发现SingletonClassT它没有构造方法。
        //下面两句代码是可以成功的因为这里没有用反射来newInstance,而是直接用的getInstance()所以没有问题。
        EnumSingleton.SingletonClassT instance4 = EnumSingleton.INSTANCE.getInstance();
        System.out.println(instance4);
        //这里通过反射的方式就会报错。
        Constructor<EnumSingleton.SingletonClassT> declaredConstructor1 = 		EnumSingleton.SingletonClassT.class.getDeclaredConstructor(null);
        declaredConstructor1.setAccessible(true);
        EnumSingleton.SingletonClassT singletonClass1 = declaredConstructor1.newInstance();
        System.out.println(singletonClass1);
    }
    //会报以下错误:
    Exception in thread "main" java.lang.NoSuchMethodException: cn.com.skyvis.Mystudy.correctSingle.EnumSingleton$SingletonClassT.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at cn.com.skyvis.Mystudy.correctSingle.TestDemoT.main(EnumSingleton.java:58)
}

我们看看枚举类的源码情况:

//EnumSingleton继承Enum
public final class EnumSingleton extends Enum
{
	//可以看到内部类SingletonClassT,完全没有构造方法。
    /* member class not found */
    class SingletonClassT {}


    public static EnumSingleton[] values()
    {
    //克隆
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(cn/com/skyvis/Mystudy/correctSingle/EnumSingleton, name);
    }

//可以看到枚举类EnumSingleton它是有参构造。不是无参构造,但是IDEA中反编译后是无参构造
    private EnumSingleton(String s, int i)
    {
        super(s, i);
        instance = new SingletonClassT(null);
    }

    public SingletonClassT getInstance()
    {
        return instance;
    }

    public static final EnumSingleton INSTANCE;
    private SingletonClassT instance;
    private static final EnumSingleton $VALUES[];

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

总结:
单例的枚举实现在《Effective Java》中有提到,因为其功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化等优点,单元素的枚举类型被作者认为是实现Singleton的最佳方法。

枚举类都是static类型的,因为static类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。

  • 我们知道,以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。
  • 但是,**为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。
  • 同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
    我们看一下这个valueOf方法:
public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) { 
T result = enumType.enumConstantDirectory().get(name); 
if (result != null) 
return result; 
if (name == null) 
throw new NullPointerException("Name is null"); 
throw new IllegalArgumentException( 
"No enum const " + enumType +"." + name); 
}
  • 从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。
  • 在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。
  • 再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。
  • 所以,JVM对序列化有保证。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值