单例模式(JAVA)

定义

属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。即:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例(所以构造方法私有)。
  • 单例类必须给所有其他对象提供这一实例(静态访问方法)。

主要解决一个全局使用的类频繁地创建与销毁这一问题

优缺点

优点:

  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例,还可以减少系统的性能开销
  • 避免对资源的多重占用(比如写文件操作)。

缺点:

  • 单例模式没有抽象层,没有接口,不能继承(构造方法私有),扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。

  •  单例类的职责过重,在一定程度上违背了“单一职责原则”。

  • 滥用单例将带来一些负面问题,如:为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;又比如:在多个线程中操作单例类的成员时,但单例中并没有对该成员进行线程互斥处理。

应用场景

  1. (状态化)WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  2. 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
  3. 要求生产唯一序列号。
  4. (无状态化)提供工具性质的功能。
  5. 。。。。。。。。

实现

饿汉式

public class Hungry {

    //饿汉式就是一上来就先把对象加载
    private final static Hungry HUNGRY = new Hungry();

    //私有构造器
    private Hungry(){ }

    //暴露给外部获取单例实例的方法
    public static Hungry getInstance(){
        return HUNGRY;
    }
}

饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。

优点:只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。

缺点:即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了,如果类里面还存在其他变量(如一个很大的数组)就更浪费内存了。

为了解决这一缺点,有了懒汉式实现

懒汉式

如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建。所以懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。

public class LazyMan {
    private LazyMan(){}
    private static LazyMan lazyMan;//区别于饿汉式,先不创建实例
    public static LazyMan getInstance(){
        if(lazyMan == null){
            lazyMan = new LazyMan();//需要的时候再创建
        }
        return lazyMan;
    }
}

但是这里的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例。

比如启多个线程时,正常情况下只会有一个实例(左),那么"创建"两个字只会打印一次,但多线程下却有多个实例(右):

 public class LazyMan {
    private static LazyMan lazyMan;//区别于饿汉式,先不创建实例

    private LazyMan(){

    }

    public static LazyMan getInstance(){
        if(lazyMan == null){
            lazyMan = new LazyMan();//需要的时候再创建
            System.out.println("创建");
        }
        return lazyMan;
    }

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

                                                              

因此需要加锁解决线程同步问题,实现如下:

public class LazyMan {
    private static LazyMan lazyMan;

    private LazyMan(){}

    public static synchronized LazyMan getInstance(){ //加锁
        if(lazyMan == null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

但这种方式效率低,第一次加载需要实例化,反应稍慢。每次调用getInstance方法都会进行同步,消耗不必要的资源。为了提高资源利用率,有了双重检查单例。

双重检查单例(DCL懒汉式)

public class LazyMan {
    private static LazyMan lazyMan;

    private LazyMan(){}

    public static LazyMan getInstance(){
        if(lazyMan == null){//避免了不必要的同步
            synchronized (LazyMan.class){
                if(lazyMan == null){//在null的情况下再去创建实例
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

第一层判断的主要避免了不必要的同步,第二层判断是为了在null的情况下再去创建实例。

但这种双重检测也存在问题,因为lazyMan = new LazyMan();不是原子操作。

lazyMan = new LazyMan();分三步:

  1. 给LazyMan实例分配内存,将函数压栈,并且申明变量类型。
  2. 初始化构造函数以及里面的字段,在堆内存开辟空间。
  3. 将lazyMan对象指向分配的内存空间。

但是以上三步并不一定按照顺序执行,因为"指令重排",会有问题,比如:

A线程按照132执行,假设刚执行完第3步,layzyMan已经分配了内存空间,但并未初始化。此时,线程B获取实例的时候,因为A的操作,B执行时会认为第一层的lazyMan != null,而直接return lazyMan,而此时lazyMan还未完成构造,是有问题的。

为了解决这一问题,必须借助volatile(保证不发生指令重排,不保证原子性):

public class LazyMan {
    private volatile static LazyMan lazyMan; //volatile

    private LazyMan(){}

    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized (LazyMan.class){
                if(lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

关于static关键字的作用:如果去掉关键字的static,那么getInstance的关键字也要被去掉,那么调用时就不能通过Single.getInstance调用,就必须new Single(),那么单例也就失去作用了,因为不同线程会自己new一个Single

静态内部类

此外还可以通过静态内部类的方式去实现。

  public class Singleton {
      private static Singleton instance;
      private Singleton() {
      }
      public static class SingletonInstance {
          private static final Singleton INSTANCE = new Singleton();
      }
      public static Singleton getInstance() {
          return SingletonInstance.INSTANCE;
      }
  }

其利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全

上述的双重检查单例是可以通过反射来破坏的!!!,如:

其结果不一样,说明存在两个实例,单例被破坏:

为了解决这一问题,需要用到枚举(反射不能破坏枚举)。

枚举

public enum EnumSingle {
    INSTANCE;

    public void fuckUSA(){
        System.out.println("freedom");
    }
}

使用 EnumSingle.INSTANCE.fuckUSA()即可执行对应方法,相对于其他单例来说枚举写法最简单,并且任何情况下都是单例的

容器单例

  public class SingletonManager {
      private static Map<String, Object> objMap = new HashMap<>();
      private SingletonManager() {
      }
      public static void putObject(String key, String instance){
          if(!objMap.containsKey(key)){
              objMap.put(key, instance);
          }
      }
      public static Object getObject(String key){
          return objMap.get(key);
      }
  }

在开始的时候将单例类型注入到一个容器之中,也就是单例SingletonManager,在使用的时候再根据key值获取对应的实例,这种方式可以使我们很方便的管理很多单例对象,也对用户隐藏了具体实现类,降低了耦合度;但是为了避免造成内存泄漏,一般在生命周期销毁的时候也要去销毁它。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值