单例模式
What
Ensure a class has only one instance, and provide a global point of access to it.
确保类只有一个实例,并提供对它的全局访问点。
- 只有一个实例
- 提供全局访问点
How
- 饿汉模式
public class Singleton {
//全局只有一个实例
private static Singleton SINGLETON = new Singleton();
//私有构造函数
private Singleton() {};
//提供全局访问点(方法)
public static Singleton getSingleton() {
return SINGLETON;
}
}
饿汉模式在类Singleton加载到jvm初始化的时候,就为static字段的SINGLETON创建了对象,由于一个类在整个生命周期中只会被加载一次,此时SINGLETON就是全局里唯一一个Singleton的实例,所以饿汉模式天生线程安全
。
- 懒汉模式
public class Singleton {
//全局只有一个实例
private static Singleton SINGLETON = null;
//私有构造函数
private Singleton(){};
//提供全局访问点(方法)
public static Singleton getSingleton(){
if ( null == SINGLETON ) {
SINGLETON = new Singleton();
}
return SINGLETON;
}
}
懒汉模式在第一次调用该类时,才加载实例,表现为延时加载。然而线程不安全
,现象一下同时有10个线程访问到SINGLETON = new Singleton();
这句代码时,就会创建10个对象,不符合单例的概念。
到这里可以发现,给方法getSingleton()
加锁,是否可以实现线程安全?
上代码(主要区别时getSingleton()这个方法是否有加锁)
public class Singleton {
//全局只有一个实例
private static Singleton SINGLETON = null;
//私有构造函数
private Singleton(){};
//提供全局访问点(方法)
public static synchronized Singleton getSingleton(){
if ( null == SINGLETON ) {
SINGLETON = new Singleton();
}
return SINGLETON;
}
}
static synchronized
相当于 synchronized(Singleton.class),
是一个类锁,所以一次只能有一个线程能调用getSingleton()
方法。
这里我们又发现一个问题,性能太低,每次访问getSingleton()
方法,都只能运行一个线程访问,但其实我们只有再第一次创建SINGLETON对象实例时,才需要加锁。怎么解决呢?这里就介绍到双重校验锁(Double Check Lock)。
- 双重校验锁模式(Double Check Lock,简称:DCL)
public class Singleton {
//全局只有一个实例
private static Singleton SINGLETON = null;
//私有构造函数
private Singleton(){};
//提供全局访问点(方法)
public static Singleton getSingleton(){
if ( null == SINGLETON ) {
synchronized (Singleton.class) {
if ( null == SINGLETON ) {
SINGLETON = new Singleton();
}
}
}
return SINGLETON;
}
}
可以看到,在每次同步之前,都会先判断SINGLETON是否已经创建,这样做的优点是:当SINGLETON已创建的时候,这个时候访问getSingleton()
的时候,就无需再加锁,提升了性能。
DCL初步保证了懒加载的线程安全,但还是不完美,为什么呢?
因为同步块里的这句代码SINGLETON = new Singleton();
,SINGELETON可能会异常。
引起这个问题的原因是:jvm的指令重排序.
解决方案:使用java的关键字volatile解决指令重排序,private static Singleton SINGLETON = null;
改为private volatile static Singleton SINGLETON = null;
怎么去理解这个问题呢?
jvm执行这句代码的逻辑步骤大概为:
1:在堆里开辟内存空间,准备开始实例化;
2:实例化Singleton;
3:SINGLETON指向堆里已实例化的Singleton;
现在假设有两条线程(线程A与线程B),线程A运行到
SINGLETON = new Singleton();
代码段时,由于jvm的指令重排序,可能先执行步骤1与步骤3,此时如果线程B运行到if ( null == SINGLETON )
时,会判断false返回一个未完全实例化SINGLETON对象,程序对其操作就会出现异常。
小小总结:
到这里已经介绍了饿汉模式与懒汉模式,饿汉模式是天生线程安全,也介绍了懒汉模式如何实现线程安全,接下来再介绍一种比较有趣实现方案。
- 静态内部类
public class Singleton {
//私有构造函数
private Singleton(){};
//提供全局访问点(方法)
public static Singleton getSingleton(){
return SingletonHolder.SINGLETON;
}
//私有内部静态类
private static class SingletonHolder {
//全局只有一个实例
private static Singleton SINGLETON = new Singleton();
}
}
有许多大佬给出很详细的解释了,各位可以自行去搜索,毕竟会搜索也是一种能力。
这里给我自己的理解:
1:可以看到内部静态类private static class SingletonHolder{}里的private static Singleton SINGLETON = new Singleton();,
类似饿汉模式,一旦内部静态类被加载,那么SINGLETON也会被创建,jvm会保证一个类只会初始化一次,也就是说,SINGLETON只会被实例化一次(因为SingletonHolder初始化的时候,被关键字static修饰的字段也会相应被创建)。
2:那么内部静态类SingletonHolder
什么时候被加载初始化呢?就是在getSingleton()
方法被调用的时候,执行return SingletonHolder.SINGLETON;
代码的时候。
3:有朋友会问,不是加载类Singleton的时候,就同时加载SingletonHolder内部静态类类吗?答案是:不会。不管是内部静态类还是内部非静态类,都不会随Singleton类的加载而加载。
When & Where
用在哪里?
举例:
- 网站访问人数;
- 配置文件对象;
- 缓存字典对象;
还有例如日志文件,回收站,打印机,连接池,线程池等等。
结束语 : 优秀是一种习惯