单例模式出现的原因
在一些情况下,一个程序中的某一个对象只需要一个就足够,例如一个系统的设置对应的对象,又或者是线程池这种一个系统只需要一个的东西,如果我们不加以控制,让其出现了多个对象,就会导致资源的浪费,更严重的,如果每个对象的操作不一致,会导致程序一些难以发现或解决的bug出现。就比如一个工作只需要一个人去做,如果让一百个人来做,反倒会七手八脚乱七八糟,那么,如何保证一个程序的某一个类的实例化对象在同一时间只存在一个呢?
单例模式的实现方法
看到这里,你的第一反应可能是使用static,毕竟,我们一听到什么东西整个程序中只需要一个首先就会想到使用static,实现单例模式确实可以使用static关键字,但是如果你寄希望于使用:
static A a1 = new A();
这样的语句来保证只有一个类A的对象那可就错了,因为我们还可以:
a2 = new A();
a3 = new A();
a4 = new A();
......
这不还是想创建几个就创建几个A类的实例对象嘛!
单例模式中的static关键字可不是这么去使用的,下面让我们具体看一下单例模式的几种实现方式。
1.经典实现方式
首先我们需要明白的一点就是,为了实现不随随便便地可以new出来多个对象的目的,我们肯定需要通过一些手段去限制“new”对象的这个过程,而这种限制的手段可能你也已经猜到了,就是在一个类的构造方法上做手脚。如果我们将一个类的构造方法设置为private,那么在这个类之外就没有办法去调用这个构造方法,也就没法随随便便地去new对象了。知道了这一点,我们接下来只要通过一些代码在类内控制这个类只有一个实例就可以了,下面我们就来看看单例模式的经典实现方式:
public class Singleton
{
private static Singleton singleton;
private Singleton()
{
}
public static Singleton getInstance()
{
if(singleton == null)
{
singleton = new Singleton();
}
return singleton;
}
}
相信你看过了代码之后自然就懂得其中的逻辑了,当对象没有被实例化的时候,实例化一个新对象出来,并把它赋给静态域存储;当对象已经被实例化过的时候,就直接返回这个对象的实例。
值得注意的是这里的两个“static”关键字,static使用的情形一般有两种,一种情形是,只想为某特定域分配单一存储空间,而不去考虑究竟要创建多少对象,甚至根本就不创建任何对象;另一种情形是,希望某个方法不与包含它的类的任何对象关联在一起,也就是说,即使没有创建对象,也能够调用这个方法。
第一个static关键字之所以存在是因为如果这里不写java会报错,一个类在初始化的时候会先初始化带有static关键字的方法或者是域,如果带有static关键字的方法中访问的域也必须带有static关键字,因为不写static关键字的域会在static方法之后加载,会造成static方法访问不到那个域,这在java语法中是不允许的;第二个static关键字存在的情形是第二种,我们现在调用这个getInstance()方法就是为了创建一个对象,所以当然要使用类名去调用这个方法。
优点
- 只有在调用这个getInstance()方法的时候才会去实例化对象,不调用方法的时候不会占用空间
- 写法简单,容易实现
缺点
- 只能在单线程的情况下实现单例模式,在多线程时,如果多个线程同时执行
if(singleton == null)
的时候,多个线程同时判断if条件为true,这样就会同时执行singleton = new Singleton();
这样多个线程就会创建多个实例,违背了我们单例模式的初衷
2.“急迫”创建对象方式
为了解决上面的经典实现方式在多线程情况下的问题,出现了一种这样的写法:
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton()
{
}
public static Singleton getInstance()
{
return singleton;
}
}
这种写法也比较好理解,我们根本不去判断类是否已经实例化过,而是直接在类加载的时候就给这个类中的static域进行了实例化。
优点
- 避免了多线程问题的出现
- 写法简单,容易实现
缺点
- 没有实现“懒加载”,只要使用这个类,无论是否使用它的getInstance()方法,都会自动实例化一个对象出来,相当于无论你用还是不用,都new了一个对象出来在那里占用空间,这样降低了内存的使用率
3.静态内部类方式
接着我们来介绍一种两全其美只是写法有些麻烦的方法:
public class Singleton {
private static class Holder
{
private static Singleton singleton = new Singleton();
}
private Singleton()
{
}
public static Singleton getInstance()
{
return Holder.singleton;
}
}
这种方法用到了内部类的知识,如果对内部类没有过了解的小伙伴可以去参阅一下内部类的知识。如果你对内部类足够了解,就会知道,无论是静态内部类还是非静态内部类,都不是在“外部”类被加载的时候进行加载的,而是只有在内部类被调用的时候才会进行加载,这就是为什么我们想到使用内部类来实现有“懒加载”效果的单例模式。
这里只有当我们调用getInstance()方法的时候,才会访问名为Holder的内部类,只有访问了这个内部类,内部类才会被加载初始化,才会实例化Singleton对象出来,同时,使用这种方式,在多线程的情况下也没有任何问题,因为只有第一次调用getInstance()方法的时候才会实例化,之后使用的都是已经实例化过的同一个对象了。
优点
- 避免了多线程问题的出现
- 实现了懒加载
缺点
- 逻辑稍微复杂
4.加锁同步方式
为了解决多线程问题,我们当然会想到使用synchronized关键字来解决问题,我们来看看代码:
public class Singleton {
private static Singleton singleton;
private Singleton()
{
}
public static synchronized Singleton getInstance()
{
if(singleton == null)
{
singleton = new Singleton();
}
return singleton;
}
}
这种方式和第一种方式唯一的区别就是在getInstance()方法前加了synchronized关键字,使用这种方式,实现了访问此方法的同步,也就不会出现同时有多个线程进入if条件判断的情况。
优点
- 避免了多线程问题的出现
- 实现了懒加载
- 写法简单
缺点
- 使用同步锁会大大降低代码效率,试想,本来程序在多线程同时运行,现在仅仅是因为调用了你的getInstance()方法就必须排队一个一个来,那当然是不太好的,因此不推荐使用这种方法
5.双重校验加锁方式
为了解决加锁同步方式带来的效率底下的问题,出现了这种写法:
public class Singleton {
private volatile static Singleton singleton;
private Singleton()
{
}
public static Singleton getInstance()
{
if(singleton == null)
{
synchronized (Singleton.class)
{
if (singleton == null)
{
singleton = new Singleton();
}
}
}
return singleton;
}
}
这种方式的要点有两个,第一个就是synchronized类锁,这里锁住了Singleton类的class对象,就相当于锁住了这个静态getInstance()方法,同时,和第4种方法不同,并不是只要访问这个方法一上来就加锁,而是必须在singleton == null
的条件成立的时候才会锁住这个方法。
第二个要点就是在域上使用了volatile关键字,说简单点,volatile就是表示修饰的东西是不稳定的、易变的。volatile作为java中的关键词之一,用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。volatile会禁止指令重排。这里使用了volatile主要是为了防止jvm指令重排序,让singleton在没有正确初始化或者被赋值的情况下就被getInstance()方法返回,导致程序错误。
优点
- 避免了多线程问题的出现
- 实现了懒加载
缺点
- 和前几种相比,这种写法贼他妈复杂啊
总结
根据上面的情况,由于第1种,第4种实现方式的弊端较大,因此不推荐使用,第2种会占用一些空间,不过在少用的情况下也是可以的,我最常用的方式是第2种和第3种。