一个静态内部类单例引发的思考

  tip:学习的一种成长方式就是多思考,由一个点去想到更多方面,多去总结别人好的设计思路,并在自己的工作中去实践。

  最近在看公司一些项目的代码,看到了使用静态内部类实现的单例写法,于是想到了单例和静态内部类这两个知识点,现在做个总结。

1、单例的实现

  单例实现有懒汉和饿汉两种方式:

  饿汉方式:如下

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

  这种方式就是类加载时就完成了实例化,时间换空间,避免多线程同步问题。

  懒汉方式:这种方式就是空间换时间,只有第一次使用时实例化,但是会增加很多判断逻辑和线程同步。

  (1)双重检查实现

public class SingletonClass { 

  private volatile static SingletonClass instance = null; 

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

  (2)静态内部类实现

public class SingletonClass {

    private static class StaticInnerClass {
        private static final SingletonClass instance = new SingletonClass();
    }

    private SingletonClass() {
    }

    public static SingletonClass getInstance() {
        return StaticInnerClass.instance;
    }

  (3)其实还有一种缓存的方式实现单例,spring容器的思路

Map<String,SingletonClass> cacheMap = new HashMap<>();

2、双重检查的思考

  (1)为什么要使用双重检查呢?

    相信很多人第一次看到都会好好思考一番,这两重检查分别是从性能和安全角度考虑的。

    同步块外层检查:这个检查是从性能当面考虑的,如果每次检查都加同步锁,显然性能是很低的,所以加这个检查保证只在第一次实例化时加锁。

    同步块内层检查:这个检查是从安全方面考虑的,例如SingletonClass有一个属性int count = 3,当线程A和B获取对象时同时进入了外层检查,然后线程A拿到了Synchronized锁,实例化了对象并进行了累加操作,此时count=4,然后线程B在线程A释放锁之后获取到了锁权限,但是不管不顾的又进行了一次实例化,此时的singleton被重新实例化,count=3,这就会出问题了。

  (2)为什么instance要使用volatile修饰

    这就涉及到volatile的原理了(请参考并发总结中的volatile原理篇),这里简单说一下,volatile有一个作用是防止指令重排,new实例化instance时,会经历如下指令过程:

                一个静态内部类单例引发的思考

 

     JVM为了提高执行效率会进行指令顺序优化,如果它认为0->7-> 4这个顺序也没问题,那就会造成所有的初始化都无效了。volatile还有一个重要作用就是内存屏障,所有使用该变量的都去共享内存去获取。所以instance使用volatile修饰是为了保证数据的一致性。

3、静态内部类的思考

  先说几个类加载的时机:

    使用new指令时若该类未加载则触发;

    反射调用某个类时该类未加载则触发;

    子类加载时若父类未加载则触发;

    程序开始时主方法所在的类会被加载;

  说完类的加载时机,就要考虑为什么静态内部类能保证线程安全:

    这是因为静态内部类只会加载一次,并且类加载过程是线程安全的。类加载的初始化阶段是单线程的,类变量的赋值语句在编译生成字节码的时候写在函数中,初始化时单线程调用这个完成类变量的赋值。

  还有一个问题就是为什么外部类加载时静态内部类没有被加载呢?

    《effective java》里面说静态内部类只是刚好写在了另一个类里面,实际上和外部类没什么附属关系,所以二者是独立加载的。

 

上一篇:14、定期和惰性一定能保证删除数据吗?如果不能,Redis会有什么应对 措施?


下一篇:指令重排测试 volatile