查看原文
其他

单例模式(下)---聊一聊单例模式的几种写法

帅地 苦逼的码农 2018-11-15

在上一篇文章 单例模式(上)---如何优雅地保证线程安全问题中,我们采取了懒汉式写法来写我们的单例模式,并且重点讲解了懒汉式中线程安全的问题。这篇我们来讲讲单例模式中的其他几种写法。

上篇文章中,方法和变量的声明都忘了加上“static”的声明,这里提醒一下。

懒汉式


懒汉式在上节我们已经讲过了,直接给出代码:


public class Singleton {
   private static volatile Singleton instance = null;
   private Singleton(){};

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

注:可以左右滑动


懒汉式这种方式需要我们来自己加锁,保证线程安全的问题。

不过就算我们保证了线程安全,这种写法还是无法保证存在唯一一个对象实例。因为别人还是可以通过反射的方式来创建一个新的对象。我写个示例:


public class Singleton {
   public static void main(String[] args) throws Exception{
       //获得构造器
       Constructor<Singleton> c = Singleton.class.getDeclaredConstructor();
       //把构造器设置为可访问
       c.setAccessible(true);
       //创建两个实例对象
       Singleton s1 = c.newInstance();
       Singleton s2 = c.newInstance();
       //比较下两个实例是否相等
       System.out.println(s1 == s2);
   }
}



打印结果:false。

所以懒汉式这种方式还是存在一些缺点的。


饿汉式


所谓饿汉式,就是一开始把对象实例创建出来,而不是等getInstance这个方法被调用才来创建对象。代码如下:


public class Singleton2 {
   private static Singleton2 instance = new Singleton2();
   //私有构造器
   private Singleton2(){};

   public static Singleton2 getInstance() {
       return instance;
   }
}



饿汉式与懒汉式相比,我们不用管线程安全的问题,代码看起来也比较简洁。

但是,由于对象一开始就被创建出来了,假如我们从头到尾都不调用getInstance()这个方法,那么这个对象就白创建了。

当然,和懒汉式一样,饿汉式也存在反射问题。

总结一下饿汉式的一些问题:

1、有可能出现对象白白浪费的情况。

2、和懒汉式一样,无法组织反射问题。


采用静态内部类的写法


直接上代码


public class Singleton3 {
   //静态内部类
   private static class LazyHolder{
       private static Singleton3 instance = new Singleton3();
   }
   //私有构造器
   private Singleton3(){};
   public static Singleton3 getInstance() {
       return LazyHolder.instance;
   }
}



由于外部类无法访问静态内部类,因此只有当外部类调用Singleton.getInstance()方法的时候,才能得到instance实例。

并且,instance实例对象初始化的时机并不是在Singleton被加载的时候,而是当getInstance()方法被调用的时候,静态内部类才会被加载,这时instance对象才会被初始化。并且也是线程安全的。

所以,与饿汉式相比,通过静态内部类的方式,可以保证instance实例对象不会被白白浪费。

但是,它仍然存在反射问题。


采取枚举的方式


直接上代码:


public enum Singleton4 {
   //一般用大写的了,不过为了和前面的统一
   //我就用小写的了

   instance;
}



枚举的方式简单吧?一行代码就搞定了,不过和饿汉式一样,由于一开始instance实例就被创建了,所以有可能出现白白浪费的情况。

但是,通过枚举的方式,不仅代码简单,线程安全,而且JVM还能阻止反射获取枚举类的私有构造器。

下面做个实验


public enum Singleton4 {
   //一般用大写的了,不过为了和前面的统一
   //我就用小写的了
   instance;

   public static void main(String[] args) throws Exception{
       //获得构造器
       Constructor<Singleton4> c = Singleton4.class.getDeclaredConstructor();
       //把构造器设置为可访问
       c.setAccessible(true);
       //创建两个实例对象
       Singleton4 s1 = c.newInstance();
       Singleton4 s2 = c.newInstance();
       //比较下两个实例是否相等
       System.out.println(s1 == s2);
   }
}



结果出现了异常:

Exception in thread “main” java.lang.NoSuchMethodException: singleton.Singleton4.()

at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.getDeclaredConstructor(Class.java:2178) at singleton.Singleton4.main(Singleton4.java:12)

所以,这种枚举的方式可以说的用的最多的一种方式了,唯一的缺点就是对象一开始就被创建,可能出现白白浪费没有用到对象的情况。

不过,总体上,还是推荐采用枚举的方式来写。

推荐阅读:

单例模式(上)---如何优雅地保证线程安全问题

一段对话讲完建造者模式

获取更多原创文章,可以关注下我的公众号:苦逼的码农,我会不定期分享一些资源和软件等。后台回复礼包送你一份时下热门的资源大礼包。同时也感谢把文章介绍给更多需要的人。

听说,点赞是一种好习惯


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存