实际编程应用场景中,有一些对象其实我们只需要一个,比如线程池对象、缓存、系统全局配置对象等。这样可以就保证一个在全局使用的类不被频繁地创建与销毁,节省系统资源。
首先要确保全局只有一个类的实例。
要保证这一点,至少类的构造器要私有化。
单例的类只能自己创建自己的实例。
因为,构造器私有了,但是还要有一个实例,只能自己创建咯!
单例类必须能够提供自己的唯一实例给其他类
就是要有一个公共的方法能返回该单例类的唯一实例。
天然线程安全:类加载时就创建了实例,占用内存空间
public class Singleton{
//static变量,全局唯一
private static Singleton instance=new Singleton();
//私有构造器:外部无法创建实例
private Singleton(){};
//公有的方法,对外提供唯一的实例
public static Singleton getInstance(){
return instance;
}
}
为方法加上锁:线程安全,但是这种做法有一个缺点,不管是不是已经存在实例了,都会被锁阻塞。效率低
public class Singleton{
private static Singleton instance;
private Singleton(){};
public static synchronized Single getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}
双重校验锁(DCL):线程安全,效率高
public class Singleton{
//volatile防止指令重排序,内存可见(缓存中的变化及时刷到主存,并且其他的内存失效,必须从主存获取)
private volatile static Singleton instance;
private Singleton(){
//构造器必须私有 不然直接new就可以创建
};
public static Single getInstance(){
//第一次判断,假设会有好多线程,如果instance没有被实例化,那么就会到下一步获取锁,只有一个能获取到,
//如果已经实例化,那么直接返回了,减少除了初始化时之外的所有锁获取等待过程
if(instance==null){
synchronized(Singleton.class){
//第二次判断是因为假设有两个线程A、B,两个同时通过了第一个if,然后A获取了锁,进入然后判断instance是null,他就实例化了instance,然后他出了锁,
//这时候线程B经过等待A释放的锁,B获取锁了,如果没有第二个判断,那么他还是会去new Singleton(),再创建一个实例,所以为了防止这种情况,需要第二次判断
if(instance==null){
//下面这句代码其实分为三步:
//1.开辟内存分配给这个对象
//2.初始化对象
//3.将内存地址赋给虚拟机栈内存中的instance变量
//注意上面这三步,第2步和第3步的顺序是随机的,这是计算机指令重排序的问题
//假设有两个线程,其中一个线程执行下面这行代码,如果第三步先执行了,就会把没有初始化的内存赋值给instance
//然后恰好这时候有另一个线程执行了第一个判断if(instance == null),然后就会发现instance指向了一个内存地址
//这另一个线程就直接返回了这个没有初始化的内存,所以要防止第2步和第3步重排序
instance=new Singleton();
}
}
}
return instance;
}
}
从代码里可以看到,做了两重的instance == null的判断,中间还用了synchronized关键字,第一个instance == null的判断是为了避免线程串行化,如果为空,就进入synchronized代码块中,获取锁后再操作,如果不为空,直接就返回instance对象了,无需再进行锁竞争和等待了。而第二个instance == null的判断是为了防止有多个线程同时跳过第一个instance == null的判断,比如线程一先获取到锁,进入同步代码块中,发现instance实例还是null,就会做new操作,然后退出同步代码块并释放锁,这时一起跳过第一层instance == null的判断的还有线程二,这时线程一释放了锁,线程二就会获取到锁,如果没有第二层的instance == null这个判断挡着,那就会再创建一个instance实例,就违反了单例的约束了。
DCL使用volatile关键字,是为了禁止指令重排序,避免返回还没完成初始化的instance对象,导致调用报错,也保证了线程的安全。
了解下singleton = new Singleton()这段代码其实不是原子性的操作,它至少分为以下3个步骤:
这里还需要知道一点,就是有时候JVM会为了优化,而做指令重排序的操作,这里的指令,指的是CPU层面的。
正常情况下,singleton = new Singleton()的步骤是按照1->2->3这种步骤进行的,但是一旦JVM做了指令重排序,那么顺序很可能编程1->3->2,如果是这种顺序,可以发现,在3步骤执行完singleton对象就不等于null,但是它其实还没做步骤二的初始化工作,但是另一个线程进来时发现,singleton不等于null了,就这样把半成品的实例返回去,调用是会报错的。
静态内部类实现:线程安全,高效
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。具体来说当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,使用INSTANCE的时候,才会导致虚拟机加载SingleTonHoler类。这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。