单例小结

起源

单例的讲解网上太多了,然而有些点很容易忘,今天又忘了双检索单例的两次条件判断的原因,一到网上查要完全弄清需要花费很长时间,这里记录一下。

双检锁

public class Singleton {
    private Singleton() {
    }
    private static volatile Singleton singleton;

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
  • 点1 : 私有构造器 为了防止该类实例化,该类只是单例的载体,没有实例化的必要。

  • 点2 : 字段singleton为volatile。防止多线程时,指令重排序造成拿到未实例化完成的对象。
    我们对singleton = new Singleton()这句话进行分析。共分为3步,
    1 在堆中分配一个空间
    2 在该空间中实例化对象
    3 让singleton字段指向该空间
    然而如果没有volatile修饰符的话,重排序有可能会先进行第三步,最后进行第二步。这样就会有一个不完全对象的引用。例如,两个线程A和B,A先进行1的操作,然后进行3的操作,此时A线程让出CPU,B线程开始调用getInstance方法,B线程会发现singleton字段已经不是null了,于是就拿到该单例的引用,但是A线程对单例的实例化还未完全结束,B线程就有可能报错。

  • 点3: 第一个判断null是为了提高性能,避免每次调用都加锁,因为对象实例化之后的调用都会发现对象不为null,就直接返回singleton了。

  • 点4: 加锁是为了避免同时有多个线程进行实例化。

  • 点5: 第二个判断null也是防止多个线程进行实例化。例如A和B两个线程都在第一个判断是否为null的地方判定为null,A线程持有锁,B线程等待A释放锁,这样当A释放锁之后,B线程获得锁,但这时候它会发现singleton已经不为null了,于是就不会再次实例化了。

对于这种方法,没有看过解释的话肯定会觉得莫名其妙,其演进的痕迹就好像我们日常开发一样,发现有漏洞于是想一个弥补措施一样,慢慢就变成了这一奇怪的结构,应该是开发人员想出来的,并不推荐使用这一方法。

静态内部类

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

        private static Object getInstance(){
            return new Object();
        }
    }
}
  • 点1 : 私有构造器 ,为了防止该类实例化。

  • 点2 : 静态内部类在被调用的时候才会被加载,实现了懒加载。

  • 点3: 类在实例化的时候虚拟机会保证线程安全性,不用我们操心。

这一方法我没记错的话JCIP中也推荐使用,而且java源码中也有例子,例如FileSystems.getDefault()方法就是用的静态内部类。推荐大家使用这种方法。

直接实例化

private static final Object singleton= new Object();
public Object getInstance(){return singleton;}
  • 点1 : 直接加载,线程安全性同样由虚拟机保证了。
    这一方法才是用的最多的方法,简单易懂。上面两种方法写的那么复杂,就是为了引入懒加载。然而大部分情况下,我们的程序是不需要这么点性能的。

枚举

public enum SingletonEnum {
    Singleton;
    
    public Object getInstance(){
        return new Object();
    }
    
    public static void main(String[] args) {
        SingletonEnum.Singleton.getInstance();
    }
}

其实,枚举的成员就是枚举对象,只不过他们是静态常量而已。上面的枚举与下面的写法等价。另外,枚举类都继承自Enum这一类,并且是final的。

final class SingletonEnum extends Enum{
	public static final SingletonEnum Singleton = new SingletonEnum();
}

枚举的效果与直接实例化效果一样,但要新建一个类,性能不如直接实例化,也不如直接实例化简洁。

总结

如果你的单例真的有延迟初始化的必要的话,例如JAVA的FileSystems类要根据操作系统加载整个Linux或Windows文件系统,大对象延迟初始化是有必要的,这时我建议你使用静态内部类而不是双检索,这也是java语言推荐的写法。如果没有延迟初始化的必要的话,直接实例化简洁明了。

附录

静态内部类延迟初始化的验证,有兴趣的可以看看:

public class Singleton {
   public static void main(String[] args) {
       Singleton.method();
   }
   private Singleton(){}
   public Object getInstance(){
       return DefalutObject.singleton;
   }
   
   public static void method(){
       System.out.println("这里是外部类的方法");
   }
   private static class DefalutObject{
       public DefalutObject(){
           System.out.println("这里是内部类的构造器");
       }
       
       static final Object singleton = getInstance();

       //加载单例对象
       private static Object getInstance(){
           System.out.println("这里内部类的单例对象加载方法");
           return new Object();
       }

   }
}

上述main方法的输出结果为下记语句,可以看到没有调用内部类的构造器,也没由调用加载单例对象的方法。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值