本文已参与「新人创作礼」活动,一起开启掘金创作之路。
基本概念
单例模式,顾名思义是指只有一个实例的对象。
我们知道在java中对象是通过new出来的,每new一次就会产生一个新的对象,那么如何限制只产生一个对象呢?
每个对象都有构造函数,每当new对象时都会调用对象的构造函数来生成对象。 此时我们可以将构造函数设置成private来限制外部的访问,同时在类内部new一个并返回给外部使用。
单例模式参考代码如下:
public class Singleton {
// 在内部先new一个出来
public static final Singleton singleton = new Singleton();
// 构造函数为私有函数,外部无法调用,也就保证了只会有一个实例。
private Singleton() {
// 构造方法
}
}
写法
- 饿汉式 (同基本概念中的代码)
public class Singleton {
// 在内部先new一个出来
public static final Singleton singleton = new Singleton();
// 构造函数为私有函数,外部无法调用,也就保证了只会有一个实例。
private Singleton() {
// 构造方法
}
public static Singleton getInstance() {
return singleton;
}
}
优点:获取对象时较快(因为类加载时就已经new出对象了),同时避免了多线程的问题。
缺点:因为在类加载时就new出来了,可能会造成内存浪费(如果没用到该单例对象,也被实例化了)。
这个方式叫饿汉式,可能是形象的比喻该汉子比较饥饿,不管三七二十一先new一个出来填饱肚子吧。
- 懒汉式
public class Singleton {
public static Singleton singleton;
private Singleton() {
// 构造方法
}
public static Singleton getInstance() {
// 延迟构造,在调用时才new对象
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
优点:在调用时才new对象,不会造成内存浪费。
缺点:构建对象的过程放在了调用的时候,获取对象时可能有一些慢。同时,该写法在多线程环境下,可能会存在多个实例,与设计不符。(如A线程执行到判断对象是否为空时,正在new对象,但是new对象需要一定的时间,此时B线程也执行到判断对象是否为空,但此时A线程还没有成功将对象创建出来,此时B就也会创新新的对象,从而存在多个实例)
针对此缺点,可以给getInstance方法加 synchronized 关键字来保证线程安全问题。
- 双重检查式
public class Singleton {
public static Singleton singleton;
private Singleton() {
// 构造方法
}
public static Singleton getInstance() {
// 第一层检查,每个线程均可访问
if (instance == null) {
// 对Singleton类对象加锁,只有一个线程可以访问。
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return singleton;
}
}
优点:只有第一次获取对象时才会进行线程同步。(相比在方法上加synchronized关键字,这种代码块的同步开销更小)
缺点:第一次加载时可能较慢;高并发环境时这种双重校验也会失效 ,参见博客文章解释。
- 静态内部类方式
public class Singleton {
private Singleton() {}
// 单例持有类
private static class SingletonHodler {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHodler.instance;
}
}
优点:该方式解决了懒汉式线程不安全问题,同时也保持了延迟加载的优点。
缺点:使用时才会加载可能在调用时有些慢。
一般在日常开发过程中,较为推荐使用此种方式来创建单例 。
扩展
在《设计模式之禅》一书中,提到了有时可能需要创建一定数量的实例。那么可以在单例模式的基础之上进行改进,如在单例类中加入集合或map来存储单例对象。 伪代码如下:
public class Singleton {
private int maxCount = 3;
private List<Singleton> instanceList = new ArrayList();
static {
for (int i = 0; i < maxCount ; i++) {
instanceList.add(new Singleton())
}
}
private Singleton() {
}
private Singleton getInstance() {
// 根据业务逻辑返回需要的对象,此处用随机代替。
int random = (int) Math.random() * maxCount;
instanceList.get(random);
}
}
这种方式的使用场景如数据库的连接(经常在配置文件中确定最大的连接数量)。个人在开发中很少用到这种创建一定数量对象的写法。
总结
| 模式 | 优点 | 缺点 |
|---|---|---|
| 单例模式 | 1、只有一个实例,减少了内存开支,节约资源。 | 1、单例类一般没有接口,扩展困难,并且在单例类中容易混杂多个业务逻辑,与类的单一职责原则有冲突。 |