单例模式(Singleton Pattern)
单例模式,从字面意思上来看,就是一个类只有一个实例。那为什么要有单例模式呢?带着这个问题往下看。
为什么要有单例模式(单例模式的应用场景)
1.需要生成唯一序列号
假设采用最简单的序列号生成方式,即连续的方式。设序列号id的初值为1,则每次生成序列号只需要将id++即可。在这种场景下,有多个实例(也就有了多个id)是没有必要的,尤其是在多线程的情况下,必须要保证id是单例的。
2.一个类的实例被频繁的调用和销毁
如果一个类被频繁的访问,例如大家熟知的DAO(数据访问对象)类,这个类的实例会被频繁的调用,如果不使用单例模式,在每次调用时,都要创建一个对象,调用完之后又被销毁;每次调用时,new一个对象都会消耗一定的时间,频繁的创建会带来更大的开销,所以,如果使用单例模式,每次都访问这一个对象,就免去了创建的开销。
3.整个项目的共享访问点或共享数据
例如,记录一个网页的访问量时,就可以将计数器设置为一个单例,每次访问页面时,都对计数器单例进行一次访问。
单例模式的定义
确保一个类只有一个实例,而且自行实例化(在一个类中 ,使用new调用了自己的构造方法),并向整个系统提供这个实例
总结为三点:
- 整个系统只有一个实例。
- 这个实例由单例类自己创建(自行实例化)。
- 整个系统(其他所有对象),都可以访问这个实例。
※单例模式的实现
实现的核心思想:将构造方法私有化(防止生成多个实例),自行实例化。
在介绍实现方式之前,先说一下延迟初始化,也就是在使用时才初始化。
1.线程不安全的懒汉式
是否为延迟初始化 | 线程是否安全 |
---|---|
是 | 否 |
public class Singleton{
private static Singleton singleton = null;
private Singleton(){
}
public static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
可以看到,在多线程的环境下,可能会创建多个实例,所以,可以将它进行改造成下面要说的线程安全的懒汉式。
2.线程安全的懒汉式
是否为延迟初始化 | 线程是否安全 |
---|---|
是 | 是 |
public class Singleton{
private static Singleton singleton = null;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
对这个方法加上互斥锁,就保证了只会创建一个实例,但是新的问题又来了,如果唯一的实例已经被创建,这个方法的互斥锁就是没有必要的,有了互斥锁,反而会降低效率,所以,再次改造,称为下面的双检索。
3. 双检索(DCL, Double-checked Locking)
是否为延迟初始化 | 线程是否安全 |
---|---|
是 | 是 |
public class Singleton{
private static volatile Singleton singleton = null;
private Singleton(){
}
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null)
singleton = new Singleton();
}
}
return singleton;
}
}
如果有多个线程进入了第一个if,则仅有一个线程可以创建实例,其他线程被阻塞,直到实例创建完成,其他实例才能进入synchronized代码块,此时实例已经创建完成,则在第二次if判断时,不会创建实例。除了创建实例的时候,有可能会有多个进程进入synchronized代码块,造成较小的阻塞开销;与线程安全的懒汉式相比,实例创建好之后,双检索的效率更高。
使用volatile修饰的原因,一个线程调用构造方法初始化实例时,指令有可能被重排序导致出错(由于对JVM了解的不多,所以以后进行补充),使用volatile修饰之后,禁止编译器对指令进行重排序。
4.饿汉式
是否为延迟初始化 | 线程是否安全 |
---|---|
否 | 是 |
public class Singleton{
private static Singleton singleton = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
}
采用饿汉式的方式,是应用了类在加载时进行初始化,这样就不能延迟初始化(不是在使用时初始化),造成了一定的资源浪费。但是这种方式足够简单。为了解决不能延迟初始化的问题,可以使用下面的静态内部类的方式解决。
5.登记式/静态内部类
是否为延迟初始化 | 线程是否安全 |
---|---|
是 | 是 |
public class Singleton{
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
private static Singleton singleton = null;
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
在Singleton被加载时,SingletonHolder没有被加载,因为将SingletonHolder声明为private,所以,只有在Singleton类的内部才可以访问,只有在访问这个类时,这个内部类才会被加载;也就是说,只有在第一次调用getInstance()时,这个内部类被加载,完成初始化操作。
6.枚举
是否为延迟初始化 | 线程是否安全 |
---|---|
否 | 是 |
public enum Singleton {
INSTANCE;
public void serverOfSingleton() {
}
}
枚举是天然的单例,这是实现单例最简单的方式。
单例模式的优势
- 在内存中仅存在一个实例,在频繁的创建和销毁时,会提高效率。
- 避免资源的多重占用,例如,避免一个文件被多个线程的不同实例同时写入。
- 给系统设置全局访问点。(这是单例模式的定义中的一个特点)