单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。 单例模式就是为了避免不一致状态,接下来就详细介绍下单例的实现方式
分别是懒汉,饿汉,静态内部类,双重检验锁以及枚举实现方式,并主要关心加载时机以及线程安全。
饿汉模式
通俗点讲,饿汉就是这个类还没被使用到的时候,实例已经创建好了,在类加载的时候就已经存在一个实例
public class Singleton {
//类加载的时候instance就已经指向了一个实例
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
JVM在加载类的时候,是单线程的,所以可以保证只存在单一的实例,所以饿汉模式是线程安全的。
懒汉模式
懒汉模式将实例化的时机放到了需要使用的时候(饿汉是类加载了就有实例),也就是“延迟加载”,相比饿汉,能避免了在加载的时候实例化有可能用不到的实例,但是问题也很明显,我们要花精力去解决线程安全的问题
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static synchronized Singleton getInstance(){
//如果还没有被实例化过,就实例化一个,然后返回
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
如果不考虑先可能就会产生多个实例,所以加了synchronized同步锁用于确保getInstance方法线程安全,但是这种方式对性能损耗非常大,所有的线程到了这个方法全得排队等着。
双重校验锁
首先要说明的是,双重检验锁也是一种延迟加载,并且较好的解决了在确保线程安全的时候效率低下的问题。
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
对比一下最原始的加同步锁那种线程安全的方法,将整个getInstance方法锁住,那么每次调用那个方法都要获得锁,释放锁,等待等等...而双重校验锁锁住了部分的代码。进入方法如果检查为空才进入同步代码块,这样很明显效率高了很多。
有人疑问为什么instance==null要判断两次吗,那我们先去掉第二次的判断。
如果两个线程一起调用getInstance方法,并且都通过了第一次的判断instance==null,那么第一个线程获取了锁,然后实例化了instance,然后释放了锁,然后第二个线程得到了线程,然后马上也实例化了instance,这就尴尬了。单例模式就失败了。
所以加上第二次判断后,先进来的线程判断了一下,哦,为空,我创建一个,然后创建一个实例之后释放了锁,第二个线程进来之后,哎?已经有了,那我就不用创建了,然后释放了锁,开开心心的完成了单例模式。
静态内部类
懒汉模式需要考虑线程安全,所以我们多写了好多的代码,饿汉模式利用了类加载的特性为我们省去了线程安全的考虑,那么,既能享受类加载确保线程安全带来的便利,又能延迟加载的方式,就是静态内部类。Java静态内部类的特性是,加载的时候不会加载内部静态类,使用的时候才会进行加载。而使用到的时候类加载又是线程安全的,这就完美的达到了我们的预期效果~
public class Singleton {
private static class SingletonHolder{
private static Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
枚举
JDK1.5提供了一个新的数据类型,枚举。枚举的出现提供了一个较为优雅的方式取代以前大量的static final类型的变量。而这里,我们也利用枚举的特性,实现了单例模式:
public enum Singleton {
INSTANCE;
}
外部调用由原来的Singleton.getInstance变成了Singleton.INSTANCE了。
注意,原来的class已经换成了enum,但是看下继承关系就能知道,其实还是一个class
而且我们可以查看Enum内部也实现了Serializable接口,所以不用考虑序列化的问题。并且加载的时候JVM能确保只加载一个实例,所以也保证了线程安全