在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。
定义与特点
单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
单例模式有 3 个特点:
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点
结构与实现
通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。
其中,单例模式有两种生成方式:懒汉式与饿汉式。懒汉式表示在类生成时没有生成实例,而在第一次调用时生成;饿汉式表示类在生成时即产生了实例。
现在我们来看实现:
饿汉式
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
可以看到,构造函数被private修饰,导致这个类无法在外部生成实例,而提供出了一个public的方法获得提前初始化完成的类内静态对象,这样能保证无论在何种情况下,使用者拿到的对象均为同一个。
懒汉式
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
我们能清楚的看到,在我们定义这个类的时候,对象是没有生成的,只有在第一次调用的时候才会生成实例。好处很明显,能够节约内存空间,坏处同样非常致命:当我们使用多线程的时候,如果多个线程同时第一次调用LazySingleton.getInstance()方法,则可能出现线程不同步的安全性问题(多个线程同时发现instance没有实例化,因此同时进行了实例化,那么返回值为不同的对象)。
这个问题当然是有办法解决的:双重锁了解一下?
线程安全的懒汉式
我先把正确的线程安全下的懒汉式方案贴出来:
public class LazySingleton {
private static volatile LazySingleton instance = null;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
发现了么?我只做了两个很小的改动:在静态对象上加了volatile关键字,在提供给外部的方法上加了synchronized关键字。
你很可能会问,为什么要加两个锁?来来来,下面我们来分析一波…
如果我们只在方法上加入synchronized关键字,代码如下:
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
诶,有点问题:假如有100个线程同时执行,那么每次执行getInstance方法时都要先获得锁再去执行方法体,如果没有锁,就得等待。耗时长,感觉像是变成了串行处理,不妥不妥。
这个时候大多数人会有这个思路:我锁住了整个方法,的确就会降低整体效率,但是我们可是有更细粒度的同步代码块嘛。
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}
}
的确,这个方法效率是增加了,但是加了这个同步锁有用么?多线程仍然会同时判断instance为null,生成不同的实例。得,白忙活。
再进一步,我两种方式都用!这回总没问题了吧:
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}
}
微微一笑表示尊敬好吧,想到这一步已经很棒了。的确它“几乎”解决了所有的问题,可惜啊就差那么一点点了。指令重排序问题你或许听说过但是没有遇到过,非常底层的一件事:
代码 instance = new LazySingleton(); 在细节上可以继续拆分为多个步骤:
- 申请一块内存空间;
- 在这块空间里实例化对象;
- instance的引用指向这块空间地址;
可惜的是这3个步骤的顺序是不定的,比如说我们按132的顺序执行,当执行到2的时候,发现instance已经指向一个地址了,那么不再认为这个变量为null,结果对象就没被实例化。因此我们还是乖乖的使用volatile关键字来避免这个问题吧!
应用场景
对于一个web开发工作者来说,通常我们如果开发网站计数器,将使用单例避免线程不同步;项目中的一些配置和属性(如文件路径,连接数据库的用户名和密码等)以及线程池/数据库连接池/日志 等,只需要在项目启动时实例化一次性,在项目其它业务需要时只要从内存中拿出来,不需要重复在内存中开辟空间,这样可以减少不必要的内存开销,提高程序的性能,尽可能避免出现内存溢出问题(OutofMemoryException)。顺便说一句,Spring框架的Bean默认都是单例的。
总结
单例模式是常用的23种设计模式中最简单也是最常用的模式。“隐藏构造方法并提供获得类的实例的方法来达到整个系统中只有此类的唯一的实现对象”是单例的核心。饿汉式一开始就创建了资源,而懒汉式虽然一开始节约了资源,但因为锁的原因导致每次访问的时候效率降低;两种方式各有利弊,不过我选择饿汉式嘿嘿嘿