Java - 设计模式 - 单例模式(防破解,实现真正意义上的单例)

Singleton mode
首先,单例模式属于创建型模式,它的实现方式也有多种

这里,先思考一下,
一些单例的实现方式是真的实现了真正意义的单例吗?
答案是:不是的。不管是是否加锁了,仍然可通过其他方式破解单例。请看分析

下面呢,我会从它的以下方面并结合代码进行分析。

  • 常见写法
  • 作用
  • 不同实现方式
  • 每个实现方式的优缺点

常见写法
写这个呢,是为了能够使初学者有一个大概的认识,可能在分析别人的代码时见过熟悉,但当时可能并不知道它是单例模式的实现。

 public class Singleton{

    private static Singleton mInstance;
    
    private Singleton(){
    
    }
    
    public static Singleton getInstance(){
        if (mInstance == null){
            mInstance = new Singleton();
        }
        return mInstance;
    }

    public void getUserInfo(){

    }
}

//外部类要访问getUserInfo()方法
Singleton.getInstance.getUserInfo();

这个呢,可能是大家经常看到的单例模式实现。

作用
它的使用场景还是比较普遍的,比如大家在分析源码时会看到单例模式,或者自己去搭建项目结构中(或者在写一个功能类时)会用上单例模式,那么用它的目的是什么呢?
可以简单分析一下上面这个例子,首先,通常我们在使用一个类的时候呢是通过new这个类,得到这个类的实例,调用类中的方法,
这里呢,构造函数是private声明的,外部不能直接new,外部要想使用只能通过getInstance()静态方法,方法中if(mInstance == null) 会直接new一个并赋值给mInstance,不为空时直接return这个实例,可以看出并不是每次都直接去new一个实例,这样就减少了它的实例的创建。

核心用作:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点

什么意思呢?就是,这个类的实例一旦创建,它就属于一个静态全局变量,它的生命周期属跟随整个应用程序,这个类只有一个实例,其他类需要访问这个类的方法,不需要再重新new一个实例对象。

不同实现方式
1.饿汉式(单例对象立即加载)

public class HungrySington {

    private static HungrySington mInstance = new HungrySington();

    private HungrySington(){
        //防止通过反射得到这个构造器,下面会讲这里判断的原因
        if(mInstance != null)
            throw new RuntimeException();
    }

    public static HungrySington getInstance(){
        return mInstance;
    }

    public void getUserInfo(){

    }

}

特点:当类加载时就会直接创建这个类的实例,也就你不管你用不用,会先创建出来(会造成空间的浪费),那么之后调用的时候就不需要再判断了,节省了运行时间,属于空间换取时间。

2.懒汉式
普通懒汉式方式:

public class LazySinglton {

    private static LazySinglton mInstance;
    
    private LazySinglton (){
        //防止通过反射得到这个构造器
        if(mInstance != null)
            throw new RuntimeException();
    }

    public static LazySinglton getInstance() {
        if (mInstance == null) {
            mInstance = new LazySinglton();
        }
        return mInstance;
    }

    public void getUserInfo(){

    }

}

但是,懒汉式存在这线程安全问题,线程不安全,需要加上同步锁

public class LazySinglton {

    private static LazySinglton mInstance;
    
    private LazySinglton (){
        //防止通过反射得到这个构造器
        if(mInstance != null)
            throw new RuntimeException();
    }

    public static synchronized LazySinglton getInstance() {
        if (mInstance == null) {
            mInstance = new LazySinglton();
        }
        return mInstance;
    }
    
    public void getUserInfo(){

    }

}

特点:懒汉式呢,会在真正用到时创建实例,节省不必要的空间浪费。
考虑多线程访问的线程安全问题,加上了同步锁,同样的,加上同步锁影响了程序执行效率。

3.双重锁

public class DoubleCheckSinglton {

    private volatile static DoubleCheckSinglton mInstance; //volatile声明
    
    private DoubleCheckSinglton (){
        //这里也要加判断,防止通过反射访问到构造器,进而创建实例
    }

    public static DoubleCheckSinglton getSingleton() {
        if (mInstance == null) {
            synchronized (DoubleCheckSinglton.class) {
                if (mInstance == null) {
                    mInstance = new DoubleCheckSinglton();
                }
            }
        }
        return mInstance;
    }
}

4.静态内部类

public class StaticSinglton {

    private StaticSinglton(){
       //这里也要加判断,防止通过反射访问到构造器,进而创建实例
    }

    //静态内部类
    private static class SingletonHolder{
        //INSTANCE是static final 类型,保证了内存中只有一个这样的实例存在,而且只被赋值一次,保证了线程安全
        private static final StaticSinglton INSTANCE = new StaticSinglton();
    }

    //只有真正调用了getInstance,才会加载这个静态内部类
    public static StaticSinglton getInstance(){
        return SingletonHolder.INSTANCE;
    }

}

特点:静态内部类在初始化过程中是不会被加载的,只有当用户调用getInstance方法时才会加载内部类,并且实例化StaticSinglton实例INSTANCE
它符合的特点是:
1.只有在需要的时候创建实例,
2.线程安全,原因是static final StaticSinglton INSTANCE,它是静态内部类只会被实例化一次,在多线程访问的情况下,线程拿到的都是同一个实例。

5.枚举

public enum EnumSinglton {

    //枚举方式
    //枚举本身就是单例对象,由JVM从根本上提供保障,避免通过反射和反序列化的漏洞创建新的对象
    //  !!!!!!!!!!!! 即使构造器私有了 ,也能通过反射去调用
    //缺点:无延迟加载

    //定义一个枚举元素,他就代表了EnumSinglton的一个实例
    INSTANCE;

    public void getUserInfo(){

    }
    
}

//外部调用
EnumSinglton.INSTANCE

特点:上面注释也标明了。

注意:

在私有构造函数中加了判断,

if(mInstance != null)
      throw new RuntimeException();

原因是通过反射,仍然可以得到构造器
在讲“反射”的时候会讲是如何通过反射去调用的。
单例的作用就是保证应用程序只有一个实例存在,而通过反射是可以获取到这个构造器,
那么可想而知,在外部创建多少的对象是不受控制了。
所以不这样做其实也就没有做到真正意义上的单例。

简单测试 如何通过反射破解单例 :

public class DecodeSinglton {

    public static void main(String[] args) throws Exception {

        HungrySington c1 = HungrySington.getInstance();
        HungrySington c2 = HungrySington.getInstance();

        System.out.println(c1);
        System.out.println(c2);
        
        //通过反射和反序列化破解单例模式

        //通过反射找到这个类
        Class<HungrySington> cls = (Class<HungrySington>) Class.forName(
                "com.interview.javacontent.designpattern.singlton.HungrySington");
        //得到这个类的构造器
        Constructor<HungrySington>  c = cls.getDeclaredConstructor(null);
        //因为我们设置HungrySington的构造器是私有的,那么我们来跳过权限检测
        c.setAccessible(true);

        HungrySington cls3 = c.newInstance();
        HungrySington cls4 = c.newInstance();

        System.out.println(cls3);
        System.out.println(cls4);
      }
 }

以上内容,如有问题,可留言修正哦,一起探讨技术,一起成长。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值