什么是单例模式?
单例模式是设计模式中最简单的形式之一。这一模式的目的是使得类的一个对象成为系统中的唯一实例。要实现这一点,可以从客户端对其进行实例化开始。因此需要用一种只允许生成对象类的唯一实例的机制,“阻止”所有想要生成对象的访问。
单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”
Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”
Why Singleton pattern
对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。
如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。一个更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的模式动机。
在网上对单例模式的优缺点有很多说法,有的还说了资源和性能方面的东西,在我看来,单例模式的出现,其实仅仅是为了解决类对象只有唯一实例的场景,至于资源和性能还有线程安全问题,都是在解决过程中需要稍加考虑,但不是这个模式的目的。所以,
单例模式主要有3个特点:
1、单例类确保自己只有一个实例。
2、单例类必须自己创建自己的实例。
3、单例类必须为其他对象提供唯一的实例。
从具体实现角度来说,就是以下三个特点:
1、一是单例模式的类只提供私有的构造函数,
2、是类定义中含有一个该类的静态私有对象,
3、是该类提供了一个静态的公有的函数用于创建或获取它本身的静态私有对象。
单例模式的实现
一般Singleton模式通常有五种实现形式:
1、懒汉模式
2、饿汉模式
3、静态内部类模式
4、枚举模式
5、双重校验锁模式
下面分别结合代码,对这五种实现模式的优缺点做比较
第一种形式:懒汉模式,也是常用的形式。所谓懒汉,就是在调用的时候才进行加载和实例化,由此看上去比较懒,所以取名懒汉。需要注意,懒汉模式的下面这种实现有线程安全问题,当有多个线程一起调用getInstance()时则可能会生成多个实例,使用过程中务必小心,如果不考虑线程安全问题,请忽略这句话
/**
* 一、懒汉,常用的写法
*/
class LazySingleton{
private static LazySingleton singleton;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(singleton==null){
singleton=new LazySingleton();
}
return singleton;
}
}
第二种形式:饿汉模式,跟懒汉式相反,它是在类初始化时,已经自行实例化。缺点:无法像懒汉模式般在使用的时候才进行加载,需要一开始就占用着资源,优点:在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的
class HungrySingleton{
private static HungrySingleton singleton=new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return singleton;
}
}
第三种形式:静态内部类模式,由于java加载的顺序是静态属性->静态代码块->非静态属性->构造方法,所以静态内部类模式也是线程安全的,由于它也不需要在加载的时候就占用着资源,线程安全的同时拥有着懒汉模式的优点
/**
* 三、静态内部类 优点:加载时不会初始化静态变量INSTANCE,因为没有主动使用,达到Lazy loading
*/
class InternalSingleton{
private static class SingletonHolder{
private final static InternalSingleton INSTANCE=new InternalSingleton();
}
private InternalSingleton(){}
public static InternalSingleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
第四种形式:枚举模式,这是Joshua Bloch的《Effective Java》(《Java高效编程指南》)推荐的一种单例模式,他有以下三个优点:
1、 自由序列化;
2、 保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量);
3、 线程安全;
public enum AnimalHelperSingleton {
INSTANCE;
private AnimalHelperSingleton(){
}
public Animal[] buildAnimalList(){
final Animal[] animals = new Animal[10];
animals[0] = new SimpleAnimal(Animal.AnimalClass.MAMMAL,
"Dog", true, Color.GRAY);
animals[1] = new SimpleAnimal(Animal.AnimalClass.MAMMAL,
"Cat", true, Color.YELLOW);
animals[2] = new SimpleAnimal(Animal.AnimalClass.AMPHIBIAN,
"Frog", true, Color.GREEN);
animals[3] = new SimpleAnimal(Animal.AnimalClass.BIRD,
"Crow", true, Color.BLACK);
animals[4] = new SimpleAnimal(Animal.AnimalClass.BIRD,
"Cardinal", true, Color.RED);
animals[5] = new SimpleAnimal(Animal.AnimalClass.ARTHROPOD,
"Mantis", false, Color.GREEN);
animals[6] = new SimpleAnimal(Animal.AnimalClass.ARTHROPOD,
"Spider", false, Color.ORANGE);
animals[7] = new SimpleAnimal(Animal.AnimalClass.MAMMAL,
"Tiger", true, Color.ORANGE);
animals[8] = new SimpleAnimal(Animal.AnimalClass.MAMMAL,
"Bear", true, Color.BLACK);
animals[9] = new SimpleAnimal(Animal.AnimalClass.BIRD,
"Owl", true, Color.BLACK);
return animals;
}
}
调用方法:
//Call singleton to build the animal list.
Animal[] animals = AnimalHelperSingleton.INSTANCE.buildAnimalList();
第五种形式:双重校验锁
<span style="color:#008200;"> </span>public class Singleton{
private static Singleton instance = null;//是否是final的不重要,因为最多只可能实例化一次。
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
//双重检查加锁,只有在第一次实例化时,才启用同步机制,提高了性能。
synchronized(Singleton.Class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}<span style="color:#008200;">
</span>
但是这种机制是存在一定问题的,双锁机制的出现是为了解决饿汉的同步问题和懒汉的性能问题,看上面的代码,简单分析下确实是解决了多线程并行进来不会出现重复new对象,而且也实现了懒加载,但是当我们静下来并结合java虚拟机的类加载过程我们就会发现问题出来了,对于JVM加载类过程不熟悉的,这里我简单介绍下,熟悉的跳过这段(当然,既然你熟悉就自然会知道双锁的弊端了)。
jvm加载一个类大体分为三个步骤:
加载阶段:就是在硬盘上寻找java文件对应的class文件,并将class文件中的二进制数据加载到内存中,将其放在运行期数据区的方法区中去,然后在堆区创建一个java.lang.Class对象,用来封装在方法区内的数据结构;
连接阶段:这个阶段分为三个步骤,
步骤一:验证,验证什么呢?当然是验证这个class文件里面的二进制数据是否符合java规范咯;
步骤二:准备,为该类的静态变量分配内存空间,并将变量赋一个默认值,比如int的默认值为0;
步骤三:解析,这个阶段就不好解释了,将符号引用转化为直接引用,涉及到指针,这里不做多的解释;
初始化阶段:当我们主动调用该类的时候,将该类的变量赋于正确的值(这里不要和第二阶段的准备混淆了),举个例子说明下两个区别,比如一个类里有private static int i = 5; 这个静态变量在"准备"阶段会被分配一个内存空间并且被赋予一个默认值0,当道到初始化阶段的时候会将这个变量赋予正确的值即5,了解了吧!好了,上面大体介绍了jvm类加载过程,回到我们的双锁机制上来分析下问题出在了哪里?假如有两个并发线程a、b,a线程主动调用了静态方法getInstance(),这时开始加载和初始化该类的静态变量,b线程调用getInstance()并等待获得同步锁,当a线程初始化对象过程中,到了第二阶段即连接阶段的准备步骤时,静态变量doubleKey 被赋予了一个默认值,但是这时还没有进行初始化,这时当a线程释放锁后,b线程判断doubleKey != null,则直接返回了一个没有初始化的doubleKey 对象,问题就出现在这里了,b线程拿到的是一个被赋予了默认值但是未初始化的对象,刚刚可以通过锁的检索!
综上,单例模式的各种实现方式都有各自的优缺点,至于如何取舍,就要看大家在项目中的实际情况决定了