单例模式概述
什么是单例模式?
确保一个类只有一个实例,并提供该实例的全局访问点。
使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。
私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。
懒汉式-线程不安全
私有静态变量被延迟实例化,这样做的好处是如果没有用到这个类,就不会实例化,从而节约资源。
public class Singleton{
private static Singleton s;
private Singleton(){}
public static Singleton getUnique(){
if(s==null)
s = new Singleton();
return s;
}
}
饿汉式-线程安全
线程不安全的原因是因为s被实例化多次,如果直接实例化s的话,就不会线程不安全。
但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。
public class Singleton{
private static Singleton s = new Singleton();
private Singleton(){}
public static Singleton getUnique(){
return s;
}
}
懒汉式-线程安全
只要对getUnique方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次s。
public class Singleton{
private static Singleton s;
private Singleton(){}
private static synchronized Singleton getUnique(){
if(s==null){
s = new Singleton();
}
return s;
}
}
但是当一个线程进入该方法后,其他试图进入该方法的线程都必须等待,即使s已经被实例化了。这会让线程阻塞时间过长,因此该方法有性能问题。
双重校验锁-线程安全
s只要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当s没有被实例化时才需要加锁。
双重校验锁先判断s是否已经被实例化,如果没有被实例化,那么才对实例化语句加锁。
public class singleton{
private volatile static Singleton s;
private Singleton(){}
public static Singleton getUnique(){
if(s==null){
synchronized(Singleton.class){
if(s==null){
s = new Singleton();
}
}
}
return s;
}
}
为什么要使用两个if语句?
在s==null的情况下,如果两个线程都执行了if语句,那么两个线程都会进入if语句块内。虽然在if语句块内有加锁操作,但是两个线程都会执行s = new Singleton()这条语句,只是先后的问题,那么就会进行两次实例化。
第一个if语句用来避免s已经被实例化之后的加锁操作,而第二个if语句进行了加锁,所以只能有一个线程进入,就不会出现s==null时两个线程同时进行实例化操作。
s采用volatile关键字修饰也是很有必要的,s = new Singleton(); 这段代码其实是分三步执行:
- 为s分配内存空间
- 初始化s
- 将s指向分配的内存地址
但是由于JVM具有指令重排的特性,执行顺序有可能变成1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程还没有初始化的实例。
getUnique()后发现s不为空,因此返回s,但此时s还未被初始化。
使用volatile可以禁止JVM的指令重排,保证在多线程环境下也能正常运行。
静态内部类实现
加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。
public class Singleton{
private Singleton(){
}
private static class SingletonHolder{
//final变量 只能赋值一次
//是静态的,随着内部类一起加载
private static final Sigleton INSTANCE = new Singleton();
}
public static Singleton getUnique(){
return SingletonHolder.INSTANCE;
}
}
当Singleton类被加载时,静态内部类SingletonHolder没有被加载进内存。
只有当调用getUnique方法从而触发SingletonHolder.INSTANCE时才会被加载,此时初始化INSTANCE实例,并且JVM能确保INSTANCE只被实例化一次。
这种方式不仅具有延迟初始化的好处,而且JVM提供了对线程安全的支持。
也算是一种懒加载。