单例模式(Singletonpattern):确保一个类只有一个实例,并提供全局访问点,这个类也被称为单例类。
单例模式的特点:
1、单例类只能有一个实例;
2、单例类必须自己创建自己的唯一实例;
3、单例类必须给所有其他对象提供这一个实例。
单例模式又细分为:饿汉式和懒汉式两种情况。
饿汉式单例类:
public class HungerSingleton {
/**
* 构造方法私有化
*/
private HungerSingleton(){}
//创建类的唯一实例
private static HungerSingleton instance = new HungerSingleton();
/**
* 提供一个获取实例的类方法
*/
public static HungerSingleton getInstance(){
return instance;
}
}
饿汉单例类在被加载时,静态变量instance就会被初始化,此时私有的构造方法会被调用,完了之后单例类的唯一实例就会被创建出来。用饿汉式来形容很形象,既然饿那么实例就创建得比较着急,在装载类是就创建了实例。
饿汉式就是用空间换时间:当类装载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间。
懒汉式单例类:
public class LazySingleton {
/**
* 构造方法私有化
*/
private LazySingleton(){}
//创建类的唯一实例
private static LazySingleton instance = null;
/**
* 提供一个获取实例的类方法
*/
public static LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
懒汉式跟饿汉式的区别就在于,当懒汉式单例类在被加载时并没有创建出instance实例,只是声明了一下。既然是懒嘛,那就创建得比较慢,当别的对象需要使用的时候,再进行创建。
懒汉式就是用时间换空间:就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
我们创建一个测试类稍微测试一下:
public class SingletonTest {
public static void main(String[] args) {
//饿汉式
HungerSingleton h1 = HungerSingleton.getInstance();
HungerSingleton h2 = HungerSingleton.getInstance();
if(h1==h2){
System.out.println("s1和s2是同一个实例!");
}else{
System.out.println("s1和s2不是同一个实例!");
}
//懒汉式
LazySingleton l1 = LazySingleton.getInstance();
LazySingleton l2 = LazySingleton.getInstance();
if(l1==l2){
System.out.println("l1和l2是同一个实例!");
}else{
System.out.println("l1和l2不是同一个实例!");
}
}
}
运行结果为:
事实证明,他们都只能存在一个实例。
接下来我们来分析一下饿汉式和懒汉式的区别:
饿汉式:加载类时比较慢,但运行时获取对象的速度比较快,线程是安全的;
懒汉式:加载类时比较快,但运行时获取对象的速度比较慢,线程是不安全的。
那么怎么来解决懒汉式的线程安全呢?
在这里我们可以使用双重检查加锁:
可以使用“双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受很大的影响。那么什么是“双重检查加锁”机制呢?
所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
“双重检查加锁”机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只只能用在java5及以上的版本。
懒汉式单例类被稍加工后:
synchronized是线程同步的修饰符,了解过多线程就知道,它的作用域越小越好。
提示:由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。