单例模式全解

1.单例模式简单介绍

单例模式,直截了当的说就是让类在整个程序运行周期里只有一个实例对象存在。
好处是针对那些庞大而又没有必要多次新建对象的类,不让其随意创建新对象,造成额外的时间和空间浪费。

2.单例模式的实现

  • 饿汉式单例模式
    饿汉式单例模式就是不管我们在程序中是否使用某个类的对象,都会创建好一个它的对象。这种情况可以应用于那些超大型的类,在程序运行过程中创建反而会拖垮程序的运行速度。
class SingleTon{
    private static SingleTon instance=new SingleTon();

    private SingleTon(){

    }

    public static SingleTon getInstance(){
        return instance;
    }
}

简单明了,也没什么好讲解的。还有一种写法就是把创建放在静态代码块内,本质上跟上述写法是一样的。

  • 懒汉式单例模式
    懒汉式单例模式就是在我们真正需要某个类的对象时才创建。可以帮助我们节省空间。
class SingleTon{
    private static SingleTon instance;

    private SingleTon(){

    }

    public static SingleTon getInstance(){
        if(instance==null){
           instance=new SingleTon();
        }
        return instance;
    }
}

乍看似乎上面代码没有问题,但是实际上在多线程的情况下,如果有多个线程同时进入了if语句内,那么就会创建多个对象。这无疑破坏了单例模式。为了验证我们的假设是否成立,开10个线程来检验一下:

 for(int i=0;i<10;i++){
            new Thread(()->{
                SingleTon instance = SingleTon.getInstance();
                System.out.println(instance);
            }).start();
        }
        com.des.creator.single.demo2.SingleTon@4295c176
        com.des.creator.single.demo2.SingleTon@4295c176
        com.des.creator.single.demo2.SingleTon@4295c176
        com.des.creator.single.demo2.SingleTon@25f209f5
        com.des.creator.single.demo2.SingleTon@4295c176
        com.des.creator.single.demo2.SingleTon@4295c176
        com.des.creator.single.demo2.SingleTon@4295c176
        com.des.creator.single.demo2.SingleTon@4295c176
        com.des.creator.single.demo2.SingleTon@4295c176
        com.des.creator.single.demo2.SingleTon@4295c176

可以看到第四个对象的就于与其它的对象不同。

继续深入,我们该如何修改代码,才能让上述代码在多线程环境下也不会出问题呢?
你可能会想到直接在方法上加上synchronized关键字让线程顺序进入方法内。没错,这样的确是可以,但是,学过锁理论的朋友应该都知道,在多线程环境之下,并发的读取同一内容是被允许的,而我们获取对象的方法就可以看成是读(只需要在第一次访问时创建对象),所以直接在方法上加synchronized会效率低下。正确的做法如下:

 public static SingleTon getInstance(){
        if(instance==null){
            synchronized(SingleTon.class){
                if(instance==null) instance=new SingleTon();
            }
            //instance=new SingleTon();
        }
        return instance;
    }

如果instance实例已经被创建,那么所有的线程都不需要顺序的进入方法内。只需要读已经存在的实例即可。如果不存在,也可以保证创建的逻辑一定是只有一个线程进入一次。

  • 枚举方式实现单例模式
    了解过java枚举的朋友应该知道,每一个枚举对象都只会在程序运行周期之内存在一次,这正好复合单例模式的需求。并且这种写法简洁而又强大,不会被反射和序列化破坏(关于反射和序列化破坏单例模式在之后介绍).
public class Demo {
    public static void main(String[] args) {
        SingleTon instance = SingleTon.INSTANCE;
        SingleTon instance1 = SingleTon.INSTANCE;
        System.out.println(instance1==instance);
    }
}



enum SingleTon{
    INSTANCE;
}

怎么样,是不是简单又明了?并且这种方式也是大名鼎鼎的《《Effective java》》作者推荐的。

3.反射和序列化破坏单例模式

  • 反射破坏单例模式
    反射可以破坏除枚举方式以外其它形式的单例模式。
public class Demo {
    public static void main(String[] args) throws Exception{
        Class t=SingleTon.class;
        Constructor constructor = t.getDeclaredConstructor();
        constructor.setAccessible(true);
        Object newInstance = constructor.newInstance();
        Object newInstance1 = constructor.newInstance();
        System.out.println(newInstance==newInstance1);
        //false
    }
}

由于构造方法被私有化了,我们不能直接通过t.newInstance()来直接获取对象。应该首先拿到类的构造方法,设置访问权限为public来创建对象。
结果打印为false,那么成功的破坏了单例模式。

那么继续思考我们可以怎么样防止呢?
只需要在类中新增一个bool类型的变量,在构造方法中加入这样一段逻辑即可。

class SingleTon{
    static boolean flag=false;
    private static class InnerClass{
        private static SingleTon instance=new SingleTon();
    }

    private SingleTon(){
        if(flag){
            throw new RuntimeException();
        }
        flag=true;
    }
}

注意一定要设置成静态的,不然这个类中每个对象新建时flag都会是false,在一个对象中改变flag的值不会影响到其它的对象。其它就没什么好说的啦,这段代码的逻辑还是很简单的。

  • 序列化破坏单例模式
    什么是序列化?直截了当的说,就是把一个对象的状态(包括属性和方法)用一串字符表示,写入I/O流或者文件当中保存起来。 要想让一个类的对象可以被序列化,需要实现Serializable接口。
public class Demo {
    public static void main(String[] args)throws Exception {
        SingleTon instance = SingleTon.getInstance();

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Administrator\\Desktop\\tmp"));

        oos.writeObject(instance);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Administrator\\Desktop\\tmp"));

        Object o = ois.readObject();

        System.out.println(o);
        System.out.println(instance);
        //com.des.creator.single.demo5.SingleTon@16b98e56
        //com.des.creator.single.demo5.SingleTon@4b67cf4d

    }

可以看到已经生成了不同的对象,破坏了单例模式。

同样的,我们也可以有方法来防止序列化破坏单例模式,我们进入readObject()方法的底层实现就可以看到,最终根据字符创建序列化对象的逻辑是这样的:
在这里插入图片描述
如果类中存在一个叫做ReadResolve的方法,那么具体的如何获取序列化对象的逻辑交给这个方法处理。那么我们只需要在类中添加这个返回,并且直接返回创建好的对象,就可以完美解决了!

private Object readResolve(){
        return InnerClass.instance;
    }

在运行一次序列化和反序列化操作的代码,发现得到的是同一个对象。

com.des.creator.single.demo5.SingleTon@4b67cf4d
com.des.creator.single.demo5.SingleTon@4b67cf4d
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值