【每日一道设计模式】单例模式

单例模式

在Java中单例模式是一种常见的设计模式,并且单例模式分为饿汉式单例、懒汉式单例。
单例的特点:

  1. 单例类只有一个实例。
  2. 单例类必须自己创建自己的一个实例。
  3. 单例必须给所有其他对象提供一个实例。

为什么要用单例?

在我们的系统中,有一些对象我们只需要一个,比如:缓存、线程池、对话框、注册表、日志对象、充当打印机、显卡等设备驱动对象。事实上,这一类对象只能有一个实例,如果创建多个实例可能导致一些问题的产生,比如:程序的行为异常、资源使用过量、或不一致性的结果。
简单来说使用单例的好处:

  • 对于频繁的使用对象,可以省略创建对象的时间开销,这对于一些那些重量级对象,这是可观的一笔开销。
  • 由于new操作次数减少,因而对系统内存的使用频率降低,这将减轻GC压力、缩短GC停顿时间。[1]

为什么不使用全局变量确保一个类只有一个实例呢?

我们知道全局变量分为静态变量和实例变量,静态变量也可以保证该类只存在一个。只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用。
但是,如果说这个对象非常消耗资源,而且程序某次的执行中一直没有用,这样就造成资源的浪费。利用单例模式的话,我们就可以实现在需要使用时才创建对象,这样就避免了不必的资源浪费。不仅仅是因为这个原因,在程序中我们要尽量避免全局变量的使用,大量使用全局变量给程序调试维护带来苦难。[2]

饿汉式单例

用容易理解话来描饿汉式:顾名思义就是饿了很多天的汉子很久没有吃饭了,如果有人请他吃饭的话,他狼吞虎咽的吃。
不管你是否创建使用,懒汉式都会在实例化的时候已经创建好了,好处是没有线程安全的问题,坏处是浪费内存空间。

public class Singleton(){
 private static Singleton singleton = new Singleton();
//私有化构造器
 private Singleton(){}
 public static Singleton getSingleton(){
   return singleton;
  }
}

从上面的代码可以看到,Singleton类将自己的构造器私有化,但调用不能使用构造器,只能使用getSingleton方法来获取对象实例。

懒汉式单例
public class LazySingleton {

  private static LazySingleton lazySingleton;

  private LazySingleton() {
  }


  public static LazySingleton getLazySingleton() {
    if (lazySingleton == null) {
      lazySingleton = new LazySingleton();
    }
    return lazySingleton;
  }
}

这种写法在单线程环境下可以使用,但是在多线程使用的情况下还是会出现多个实例。

懒汉式同步方法

public class LazySingleton {

  private static LazySingleton lazySingleton;

  private LazySingleton() {
  }


  public static synchronized LazySingleton getLazySingleton() {
    if (lazySingleton == null) {
      lazySingleton = new LazySingleton();
    }
    return lazySingleton;
  }
}

懒汉式同步代码块

public class LazySingleton {

  private static LazySingleton lazySingleton;

  private LazySingleton() {
  }


  public static  LazySingleton getLazySingleton() {
    if (lazySingleton == null) {
      synchronized(LazySingleton.class){
      lazySingleton = new LazySingleton();
      }
    }
    return lazySingleton;
  }
}

懒汉式双重检查锁

public class LazySingleton {

  private static LazySingleton lazySingleton;

  private LazySingleton() {
  }


  public static LazySingleton getLazySingleton() {
    if (lazySingleton == null) {
      synchronized (LazySingleton.class) {
        if (lazySingleton == null) {
          lazySingleton = new LazySingleton();
        }
      }
    }
    return lazySingleton;
  }
}

双重检查锁对于多线程开发者来说并不陌生,我们进行了两次if(singleton == null)判断,并通过将实例singleton设置为volatile变量,这样可以实现变量的可见性并且禁止编译器指令重排序造成的其它问题。

如果用反射能不能破单例了?
答案是能,因为反射可以获取到无视私有构造创建对象实例。

  public static void main(String[] args) throws Exception {
    //单例模式的实例对象
    LazySingleton lazySingleton = LazySingleton.getLazySingleton();
    //无参构造
    Constructor<LazySingleton> declaredConstructor = LazySingleton.class.getDeclaredConstructor(null);
    //无视私有构造器
    declaredConstructor.setAccessible(true);
    //反射创建实例
    LazySingleton lazySingleton2 = declaredConstructor.newInstance();
    System.out.println("单例创建实例:"+lazySingleton);
    System.out.println("反射创建实例"+lazySingleton2);
  }

上面代码是需要懒汉式单例的,而且上面的运行日志的结果

单例创建实例:com.cumd.Day.LazySingleton@7f31245a
反射创建实例com.cumd.Day.LazySingleton@6d6f6e28

可以看到通过反射创建的实例和单例创建的实例是完全不同的地址,按照单例来说应该两个创建的对象地址是一样的才对。

因此得出结论,反射能够破解单例。
那么有没有办法避免反射破解单例呢?
因为反射走的是无参构造,可以通过在无参构造里判断并抛出运行时异常。

public class LazySingleton {

  private static LazySingleton lazySingleton;

  private LazySingleton() {
    synchronized (LazySingleton.class){
      if (lazySingleton==null){
          throw new RuntimeException("不要试图用反射破解单例");
      }
    }
  }


  public static LazySingleton getLazySingleton() {
    if (lazySingleton == null) {
      synchronized (LazySingleton.class) {
        if (lazySingleton == null) {
          lazySingleton = new LazySingleton();
        }
      }
    }
    return lazySingleton;
  }


  public static void main(String[] args) throws Exception {
    //单例模式的实例对象
    LazySingleton lazySingleton = LazySingleton.getLazySingleton();
    //无参构造
    Constructor<LazySingleton> declaredConstructor = LazySingleton.class.getDeclaredConstructor(null);
    //无视私有构造器
    declaredConstructor.setAccessible(true);
    //反射创建实例
    LazySingleton lazySingleton2 = declaredConstructor.newInstance();
    System.out.println("单例创建实例:"+lazySingleton);
    System.out.println("反射创建实例"+lazySingleton2);
  }


}

当使用反射创建实例的时候就会抛出Exception in thread “main” java.lang.RuntimeException: 不要试图用反射破解单例;
如何从根本上解决反射破解单例呢?
通过查看到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);
            }
        }
        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;
    }

枚举
借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

public enum Singleton{
    INSTANCE;
    public void whateverMethod(){
        
    }
}

引用来源
[1]、[2]:深入理解单例模式——只有一个实例

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值