欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

单例模式(JAVA)

程序员文章站 2022-05-01 16:14:56
定义属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。即:单例类只能有一个实例。单例类必须自己创建自己的唯一实例(所以构造方法私有)。单例类必须给所有其他对象提供这一实例(静态访问方法)。主要解决一个全局使用的类频繁地创建与销毁这一问题。优缺点优点:在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁....

定义

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

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

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


优缺点

优点:

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

缺点:

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

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

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


应用场景

  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();
        }
    }
}

单例模式(JAVA)单例模式(JAVA)

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

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;
    }
}

静态内部类

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

 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就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全


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

单例模式(JAVA)

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

单例模式(JAVA)

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


枚举

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值获取对应的实例,这种方式可以使我们很方便的管理很多单例对象,也对用户隐藏了具体实现类,降低了耦合度;但是为了避免造成内存泄漏,一般在生命周期销毁的时候也要去销毁它。

本文地址:https://blog.csdn.net/qq_33189961/article/details/108225200

相关标签: Java 单例模式