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》里面说静态内部类只是刚好写在了另一个类里面,实际上和外部类没什么附属关系,所以二者是独立加载的。