Java 设计模式--单例模式

本设计模式资料源于慕课网,讲师:Geely 《Java设计模式精讲 Debug方式+内存分析》,本人作为自己手打整理作为学习的归纳总结。

一,模式介绍

1,定义和类型

保证一个类仅有一个类,并提供一个全局访问点
类型:创建型

2,适用场景

想确保任何情况下都只有一个实例

3,优点

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

4,缺点

  • 没有接口,扩展困难

5,重点

  • 私有构造器
  • 线程安全
  • 延迟加载
  • 序列化和反序列化
  • 反射

6,相关设计模式

  • 单例模式和工厂模式
  • 单例模式和享元模式

二,代码演示

1,懒汉式

  • 懒汉式可以按照字面意思这么理解,现在要完成一个事情(指返回单例),但是这个办事的人就比较懒(指生成单例的这个类),于是这个人就只把要生成的类先给你设置成 null ;等到你要用的时候他才给你 new 出来。要不然就一直放在那。
  • 这里值得注意的是同步锁的问题,同一时间只能有一个线程去调用到这个类的生成方法,确保获取的是同一个对象,或者也可以说是不用重复生成同一个类。
package com.try1.design.creational.singleton;

/**
 * 懒汉式
 */
public class LazySingleton {
    private static LazySingleton lazySingleton=null;
    private LazySingleton(){

    }
    public synchronized static LazySingleton getInstance(){
        if (lazySingleton==null){                //检查
            lazySingleton=new LazySingleton();   //生成
        }
        return lazySingleton;
    }
}

  • 这样的写法就是最基础的懒汉式版本,但同时会又有另外一个问题,现在假象有一个线程 A 运行到了生成之前,对,想象成这个线程读这个A只读了一半,还没读到 new 呢,然后另外一个线程 B 悄咪咪的到了检查这一步,一看,行,原来还没有哥们new 啊,然后顺理走到了里面 new 的这一步,这个时候 A 已经创建了单例 A 。但是这个线程 B 也会创建单例 B。再如果这个线程 A 的手脚比较快的话,就直接返回给了系统这个单例 A ,然后我们友善的线程 B 说不对啊,我也辛苦了半天,肯定也会返回给系统这个单例 B。这样很显然,我们一个系统就同时存在了单例 A 和单例 B,故不符合单例模式的设计原则。
  • 所以我们有下面这种写法
package com.try1.design.creational.singleton;


/**
 * 懒汉式双重检查
 */
public class LazyDoubleCheckSingleton {
    //volatile 是为了防止重排序的发生。
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton=null;
    private LazyDoubleCheckSingleton(){

    }
    public synchronized static LazyDoubleCheckSingleton getInstance(){
        if (lazyDoubleCheckSingleton==null){                     //1,是为了给后来的哥们看的,是否还可以去竞争
            synchronized (LazyDoubleCheckSingleton.class){       //2,这里只能有一个哥们进去
                if(lazyDoubleCheckSingleton==null){              //3,给第一批进去的哥们看是否还有机会了
                    lazyDoubleCheckSingleton=new LazyDoubleCheckSingleton(); //4,相亲中
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

  • 这种写法名为双重检查锁-懒汉式,这样的写法是特地对上述那种情况进行的设计。
  • 现假想有现在这样一种情况,假设这是一个大型相亲现场,那姑娘俏啊,感情一大群年轻小伙唰一下就过来了,然后到了 1 这个点,这个时候女方父亲一看,这不行啊 ,我闺女一下怎么面对这么一大群人,kua一下,就把门给关上了,然后再门口坐着了,然后立了个牌子,大意就是我闺女还没被拐走,对应1这个 flag ,但是可能关门的速度不够快,还是有几个速度快的小伙挤进去了,但是里面的话,女方母亲还在姑娘房门前呢,看这几个年轻人这么努力,想了想,好吧,你们可以一个一个进去跟我闺女聊,就,随机放了一个年轻人进去了,然后也立了个跟女方父亲一样的牌子,对应 3 这个 flag 。再假设好吧,这个随机小伙还挺不错,进去一下子就获取了姑娘的芳心(这还是挺好啊),然后母亲一看女儿没了,伤心之余还是把门口的牌子,改成了闺女没了,母亲门口第一批进去的小伙一看,还是缘分不够啊,一下人就走完了,外面老父亲也听到了这个噩耗,伤心之余还是没忘记把门口的牌子改成了闺女没啦,门口小伙,一看也走完了。之后有想法的年轻人,一过来看到的就是这个牌子,还有老父亲要杀人的眼神,于是就溜了溜了。
  • 咳咳,这样就确保了只会有一个小伙子和姑娘好上了,符合了单例模式的原则,和之前写法相比,改进的是,确保了检查的人就是最后创建的人,就是多了母亲这个角色,不会让自己闺女在有了男朋友的情况下,还让她和别的小伙相亲。
  • 线程测试方法
package com.try1.design.creational.singleton;

public class T implements Runnable {
    @Override
    public void run() {
        LazySingleton lazySingleton=LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+" "+lazySingleton);


//        ContainerSingleton.putInstance("object",new Object());
//        Object instance=ContainerSingleton.getInstance("object");
//        System.out.println(Thread.currentThread().getName()+" "+instance);

    }
}

测试

//        LazySingleton lazySingleton=LazySingleton.getInstance();

//        Thread t1=new Thread(new T());
//        Thread t2=new Thread(new T());
//        t1.start();
//        t2.start();
//        System.out.println("program end!");

2,饿汉式

  • 按照文本意思也可以这么理解,来者不拒,我先创建好,你要我就给你。
package com.try1.design.creational.singleton;

import java.io.Serializable;

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

  • 这个算是最基础的版本,这个版本在于对序列化和反射都没有控制。
/*
序列化测试
 */
        HungrySingleton instance=HungrySingleton.getInstance();
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);

        File file=new File("singleton_file");
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file));

        HungrySingleton newInstance= (HungrySingleton) ois.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance==newInstance);
  • 这里返回的就不是同一个对象了,因为在利用序列化的时候,判断产生的是另外一个对象。
    在这里插入图片描述
  • 我们可以让类实现序列化接口来防止序列化生成的不是同一个对象
    防止序列化破坏后的版本
package com.try1.design.creational.singleton;

import java.io.Serializable;

public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton;
    private static boolean flag=true;
    static{
        hungrySingleton=new HungrySingleton();
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    //重写readResolve方法,使得判断返回同一个对象
    private Object readResolve(){
        return hungrySingleton;
    }
}

在这里插入图片描述
再来是反射的一个问题,我可以通过反射来创建一个新的类,这个类因为不走里面的方法同样也是不同的类。

        /*
        反射攻击测试
         */
        Class objectClass=HungrySingleton.class;
        Constructor constructor=objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);

        HungrySingleton instance=HungrySingleton.getInstance();
        //用来修改反射内部的静态常量
//        Field flag=instance.getClass().getDeclaredField("flag");
//        flag.setAccessible(true);
//        flag.set(instance,true);
        HungrySingleton newInstance= (HungrySinglet   on) constructor.newInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance==newInstance);

这里的 flag 是用作判断的一个参数,可以结合下方的单例类来看
在这里插入图片描述

package com.try1.design.creational.singleton;

import java.io.Serializable;

public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton;
    private static boolean flag=true;
    static{
        hungrySingleton=new HungrySingleton();
    }

    private HungrySingleton(){
        if (hungrySingleton!=null){
            throw new RuntimeException("单例构造器禁止反射调用!");
        }
    }

    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    //重写readResolve方法,使得判断返回同一个对象
    private Object readResolve(){
        return hungrySingleton;
    }
}

3,Enum枚举类

这个类就很厉害了,上面我们都是手动添加各种配置来防御序列化和反射,这货就直接自带了。

package com.try1.design.creational.singleton;

public enum EnumInstance {
    INSTANCE{
        protected  void printTest(){
            System.out.println("Geely Print Test");
        }
    };
    protected abstract void printTest();
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance(){
        return INSTANCE;
    }
}

同样给出测试代码

        EnumInstance instance=EnumInstance.getInstance();
        instance.setData(new Object());
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);

        File file=new File("singleton_file");
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file));

        EnumInstance newInstance= (EnumInstance) ois.readObject();

        System.out.println(instance.getData());
        System.out.println(newInstance.getData());
        System.out.println(instance==newInstance);
  • 序列化测试截图
    在这里插入图片描述
  • 反射测试截图

    大概意思是不能反射这个类。

三,总结

单例模式,作为最容易理解,最常见,但是也最难用好的一个设计模式,使用的时候一定要把各种可能的情况想到,然后根据实际需要来选择写法;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值