首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >今天再来聊聊单例设计模式

今天再来聊聊单例设计模式

作者头像
故里
发布2020-11-25 16:03:22
2440
发布2020-11-25 16:03:22
举报
文章被收录于专栏:故里学Java故里学Java

今天我们再来探讨一下单例设计模式,可以说,单例设计模式在面试考察中是最常出现的,单例模式看似简单,每个人可能都可以写出来,但是能不能写好就是一个问题,往深了考察,又能考察出面试者对于并发、类加载、序列化的掌握程度。

单例有多种写法,但是哪种写法更好的,为什么好呢,这些问题我们都会在今天的文中一一解读,首先我们需要知道什么是单例模式。

什么是单例模式?

单例模式指的是,保证一个类只有一个实例,并且提供一个全局可以访问的入口。

那么我们为什么需要单例呢?

其中第一个理由就是节省内存、节省计算。在我们平时的程序中,我们很多时候就只需要一个实例就够了,如果出现了更多的实例反而属于浪费。

举个例子,我们就拿一个初始化比较耗时的类来说,在这个类构造的时候,需要查询数据库,并对查到的数据做大量的计算,所以在第一次构造的时候,我们花了很多时间来初始化这个对象,假设我们数据库里的数据是不变的,并且把这个对象保存在了内存中,那么以后就可以使用同一个实例了,如果每次生成新的实例就没有必要了。

第二个理由就是为了保证结果的正确,比我们需要一个全局的计数器,用来统计人数,如果有多个实例,反而可能会造成混乱。

第三个理由就是方便管理,很多工具类我们只需要一个实例,我们通过一个统一的入口,比如通过getInstance方法,就可以获取到这个单例,这是很方便的,太多的实例不但没有帮助,反而会显得有点混乱。

单例模式有哪些使用场景?

  • 无状态工具类:如日志工具类、字符串工具类...
  • 全局信息类:如全局计数、环境变量类...

常见单例模式的写法:

主要有五种:饿汉式、懒汉式、双重检查式、静态内部类式、枚举式。接下来根据难度依次展开讲述:

1. 饿汉式
public class Singleton{
    private static Singleton singleton = new Singleton();
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        return singleton;
    }
}

我们来看看饿汉式的写法,用static修饰我们的实例,并且把构造函数用private修饰,这种写法比较简单,在类装载的时候就完成了实例化,避免了线程同步的问题,缺点就在于类装载的时候就完成了实例化,没有达到懒加载的要求,如果从始至终都没有使用过这个实例,就可能会造成内存的浪费。

还有一种方式与饿汉式比较类似,就是静态代码块式;

public class Singleton{
    private static Singleton singleton;
    
    static{
      singleton = new Singleton();
    }
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        return singleton;
    }
}

这种方式和饿汉式类似,只不过把类实例化的过程放到了静态代码块中,也是在类装载的时候就执行了静态代码块中的代码,完成了实例化。所以这种方式和饿汉式的优缺点也是一样的。

2. 懒汉式

在了解了饿汉式的缺点之后我们来看看第二种写法,懒汉式,这种写法在getInstance方法被调用的时候,才去实例化我们的实例,但是只能在单线程下使用。如果在多线程下使用,如果一个线程进入了if(singleton == null)判断语句块,还没来得及往下执行,另一个线程也通过了这个判断语句,这时就会多次创建实例。所以这里需要注意,多线程环境下不能使用这种方式。

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

如果要保证线程安全,我们可以对前边的写法进行升级,线程安全的懒汉式是怎么样的呢,我们可以在getInstance方法上加synchronized关键字,这样就可以解决上边出现的线程安全问题。不过就是效率太低了,每个线程在获得类的实例的时候,执行getInstance方法,都要进行同步,多个线程不能同时访问,然而这在大多数情况下都是没有必要的。

public class Singleton{
    private static Singleton singleton;
    
    private Singleton(){}
    
    public static synchronized Singleton getInstance(){
      if (singleton == null) {
        singleton = new Singleton();
      }
        return singleton;
    }
}

这个地方有人会说,把synchronized关键字加在方法上效率太低了,那么缩小范围,把synchronized从方法上移除,然后把synchronized关键字放到了我们的方法内部,采用了代码块的形式来保证线程安全,不过这种方法是有问题的,有可能产生多个实例。加入一个线程进入了第一个if(singleton == null)判断语句块,还没来得及往下执行,此时又一个线程通过了这个判断,此时就会产生多个实例。

public static Singleton getInstance(){
      if (singleton == null) {
        synchronized (Singleton.class) {
          singleton = new Singleton();
        }     
      }
        return singleton;
}
3. 双重检查模式

双重检查模式的出现就是为了解决上边出现的问题,就有了双重检查模式。

public class Singleton{
    private static volatile Singleton singleton;
    
    private Singleton(){}
    
    public static Singleton getInstance(){
      if (singleton == null) {
        synchronized (Singleton.class) {
          if (singleton == null) {
            singleton = new Singleton();
          }         
        }   
      }
        return singleton;
    }
}

我们重点来看一下getInstance方法,我们进行了两次singleton == null的判断,就可以保证线程安全了,这样实例化代码只用调用一次,后面再次访问的时候,只会判断第一次的if (singleton == null)就可以了,然后会跳过整个if块,直接返回实例化对象,这种写法的好处就是,不仅线程安全,而且延迟加载,效率也更高。

这里就会出现一个面试题,为什么是两次判断,去掉第二次的if判断行不行?

这个时候需要考虑这样一种情况,有两个线程同时调用了getInstance方法,并且由于singleton是空的,所以两个线程都可以通过第一个if判断,然后由于锁机制,会有一个线程先进入同步语句,并进入第二个if判断,而另外一个线程需要等待锁释放,不过当第一个线程执行完new Singleton()语句后,就会退出synchrinized保护的区域,这时如果没有第二个判断,那么第二个线程也会创建一个实例,这就破坏了单例。那么去掉第一个判断,所有的线程都会串行执行,效率低下。综上,两个判断都是需要保留的。

还有一个点需要注意,我们给Singleton对象加了volatile关键字修饰,这又是为什么呢?

这主要在于singleton = new Singleton()这句,这并不是一个原子操作,在JVM中,这条语句至少做了三件事,第一步,给singleton分配内存空间;第二步,调用Singleton的构造函数等来初始化singleton;第三步,讲singleton对象指向分配的内存空间(执行完这一步singleton就不是null了)。

这个地方需要注意一下1-2-3的顺序,存在着重排序的优化,也就是说第二步和第三步的顺序不能保证的,最终的执行顺序可能是1-2-3,也可能是1-3-2。

如果顺序是1-3-2,那么第3步执行完之后,singleton就不是null了,可是此时并没有执行第2步,假设此时又有一个线程进入了getInstance方法,由于此时的singleton已经不是null了,就会通过第一个判断,直接返回对象,其实这个时候的singleton并没有完成初始化,所以使用这个实例的时候就会报错。

使用volatile的意义就在于,它可以防止上边出现的那种重排序的发生,也就避免了拿到未完成初始化的对象。

4. 静态内部类

静态内部类的方式和饿汉式采用的机制有点类似,都采用了类装载的机制,来保证我们初始化实例时只有一个线程。所以在这个地方是由JVM实现的一个线程安全。

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

饿汉式 的方式,在类被加载的时候,就会实例化对象,而静态内部类方法在Singleton类被装载时,并不会被立刻实例化,只有在调用getInstance方法的时候,才会进行实例化。

看到这里我们已经学会了双重检查和静态内部类两种方法来线程 安全、高效、延迟加载的创建单例,这两种方式都是不错的写法,但是它们不能防止被反序列化,生成多个实例。

5. 枚举法

借助枚举类来实现单例,这不仅能避免多线程同步的问题,而且还能反正反序列化,和反射创建新的对象,来破坏单例情况的出现。

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

至此,我们已经学了五种方法实现单例,但是怎么选择呢,其实还是优先推荐枚举法,还是要看看枚举写法的优点,枚举写法的优点:

其一是写法简单,不需要我们去考虑线程安全和懒加载,代码也比较短小精悍,是最简练的写法。

其二是线程安全有保障,通过反编译一个枚举类,我们发现枚举类中的各个枚举项是通过static代码块来定义和初始化的,它们会在类加载的时候完成初始化,而Java类的加载由JVM保证线程安全,所以创建一个Enum类型枚举类是线程安全的。前面几种实现单例的方式都存在一些问题,那就是可能被反序列化破坏,反序列化生成的新的对象从而产生了多个实例。

其三是防止破坏单例,Java对于枚举的序列化做了要求,仅仅是将枚举类对象的name属性输出到结果中,在反序列化时,就是通过java.lang.Enum的valueOf方法,来根据名字查找对象,而不是新建一个新的对象,所以这就防止了反序列化导致的单例破坏问题的出现。对于反射破坏单例的问题,枚举同样有措施,反射在通过newInstance创建对象时,会检查这个类是不是枚举类,如果是枚举类就抛出illegalArgumentException("Cannot reflectively create enum objects")异常,反射创建对象失败。可以看出枚举是可以防止反序列化和发射破坏单例。这就是枚举在实现单例上的优势。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 故里学Java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是单例模式?
  • 那么我们为什么需要单例呢?
  • 单例模式有哪些使用场景?
    • 1. 饿汉式
      • 2. 懒汉式
        • 3. 双重检查模式
          • 4. 静态内部类
            • 5. 枚举法
            相关产品与服务
            文件存储
            文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档