单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式有以下特点:
①单例类只能有一个实例。
②单例类必须自己创建自己的唯一实例。
③单例类必须给所有其他对象提供这一实例。
①单例类只能有一个实例。
②单例类必须自己创建自己的唯一实例。
③单例类必须给所有其他对象提供这一实例。
所谓单例模式,简单来说,就是在整个应用中保证只有一个类的实例存在。就像是Java Web中的application,也就是提供了一个全局变量,用处相当广泛, 比如保存全局数据,实现全局性的操作等。
理解了什么是单例模式,下面我们来用浅入深介绍它的实现方式:
1、饿汉模式(最简单的实现):
public class Singleton{
//定义一个私有静态的成员变量,调用自身构造方法给其赋初值
private static final Singleton singleton = new Singleton();
//定义私有构造方法
private Singleton(){};
//提供一个供外部放问本类的静态方法,由类名直接调用
public static Singleton getSingleton(){
return singleton;
}
}
分析:
优点:
饿汉模式在
类加载时就已经创建好了一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的;
缺点:饿汉模式像一个贪心不足的胖子,因为不管这个类是否被使用,都会创建一个实例,如果这个创建过程很耗时呢,比如连接1000次数据库(夸张了..),这必然影响到了系统性能。
2、懒汉模式(优化性能)
public class Singleton{
//定义一个私有静态的成员变量,初始值为null
private static Singleton singleton = null;
//定义私有构造方法
private Singleton(){};
//提供一个供外部放问本类的静态方法,由类名直接调用
public static Singleton getSingleton(){
//第一次调用时创建一个实例
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
分析:
代码的变化有两处:①singleton初始化为null,直到第一次使用的时候判断是否为null来创建对象,因为创建过程不在声明处或构造方法中,所以final必须去掉。
优点:类加载时不会生成实例,只有在第一次调用getSingleton()方法时创建实例,之后再调用则使用第一次创建的实例。
缺点:懒汉模式是线程不安全的。
场景:在多线程的环境下,线程A希望使用Singleton类,调用getSingleton()方法,因为是第一次调用,A就发现singleton是null,于是它开始创建实例——就在这个时候,CPU发生时间片切换,线程B 开始执行,它要使用Singleton类,调用getSingleton()方法,同样检测到singleton为null(因为这是在A检测完之后切换的,也就是说A并没有来得急创建对象),B开始创建对象,B创建完后,切换到 A继续执行,因为A已经检测完了,它不会重新检测一遍而是直接创建对象。这样一来,线程A和B各自拥有一个Singleton的对象,单例失败!
3、懒汉模式变种之一(线程安全的实现)
public class Singleton{
//定义一个私有静态的成员变量,初始值为null
private static Singleton singleton = null;
//定义私有构造方法
private Singleton(){};
//提供一个供外部放问本类的静态方法,由类名直接调用
public synchronized static Singleton getSingleton(){
//第一次调用时创建一个实例
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
分析:
优点:给getSingleton()方法加上同步锁,一个线程必须等到另外一个线程执行完这个方法后才能调用这个方法,保证了实例的唯一性。
缺点:synchronized修饰的同步块要比一般的代码块慢好几倍,如果多次调用getSingleton()方法,性能势必会受到影响。
4、懒汉模式变种之二(线程安全的实现 + 性能优化)
public class Singleton{
//定义一个私有静态的成员变量,初始值为null
private static Singleton singleton = null;
//定义私有构造方法
private Singleton(){};
//提供一个供外部放问本类的静态方法,由类名直接调用
public static Singleton getSingleton(){
if(singleton == null){
synchronized(Singleton.class){
//第一次调用时创建一个实例
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
分析:
我们来分析一下懒汉模式失败的原因,原因是检测到null的操作与创建实例的操作分离了,我们只要保证这两个操作能够原子的进行就能够保证单例的成功。但是此时性能并没有得到优化!
我们发现只有当singleton为null时才会有可能发生单例失败的情况,所以我们在同步代码块之外再加一个if(singleton == null)的判断,这样只有在第一次调用getSingeton()方法时才执行同步代码块中的语句,使性能得到优化。
优点:在变种一的基础上优化了代码性能。
缺点:同步块是非常慢的,因此不使用同步块,就能进一步优化性能。
5、懒汉模式变种之三(线程安全的实现 + 性能优化 + 性能优化)
public class SingletonClass {
private static class SingletonClassInstance {
private static final SingletonClass instance = new SingletonClass();
}
public static SingletonClass getInstance() {
return SingletonClassInstance.instance;
}
private SingletonClass() {
}}
分析:
在这一版本的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明确说明了的,因此不存在任何二义性。在这段代码中,因为SingletonClass没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载SingletonClassInstance类,这个类有一个static的SingletonClass实例,因此需要调用SingletonClass的构造方法,然后getInstance()将把这个内部类的instance返回给使用者。由于这个instance是static的,因此并不会构造多次。
由于SingletonClassInstance是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。
总结:
以上介绍了5种单例模式的实现:懒汉模式与饿汉模式,以及懒汉模式为了实现线程安全的三个变种。
饿汉模式在类加载时同时实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定内存;
懒汉模式会延迟加载,在第一次使用单例时才会创建单例,杜绝了内存浪费,但第一次需要加载实例,所以会慢一点,但是之后就跟饿汉模式一样了。
变种一:在方法上加了同步,虽然线程安全了,但是每次都需要调用同步代码块,会影响性能,因为发生线程危机的概率并不高,所以导致性能浪费。
变种二:将判断为null与创建实例放在了同步块中,并通过检测是否为null来决定是否执行同步块,这样的双重验证锁机制即保证了线程安全,又避免了每次都需要执行同步而导致的性能浪费。
变种三:利用了类加载机制保证单例,而且不使用同步块进一步优化了性能。