定义
单例模式是一种应用广泛的模型,适用于一个类全局只能有一个实例的情况。比如说,在一个教学系统中我们定义了一个班长类,但是一个班只能有一个班长。那么我们需要限定一下,其他类不能随便创建班长对象,只能获取本班的班长。这就是单例模式。它需要满足以下几个特征:
- 系统创建,不能由外部随便创建;
- 只能创建一次,不能重复创建;
- 外部只能调用。
下面逐个分析如何达到这些特征。首先是系统创建,这就要求构造函数私有化,只有这样,外部就无法通过new创建对象了,因为new其实就是调用了构造函数。只能创建一次意味着在每次调用实例前我们需要判断一下有没有被创建,若创建过了则直接返回已经创建好的实例对象,否则内部自行创建。外部调用要靠公有函数实现,且外部只能通过公有函数接口访问对象。这也说明了对象应该是一个封装的私有对象。
最简单的单例模式实现如下:
public class Singleton {
private Singleton() { //不可随意创建对象
System.out.println("创建单例模式: " + this); //测试,创建成功后输出对象哈希码
}
//饿汉式,创建后直接分配内存
private static Singleton instance = new Singleton();
//获取对象的公有接口
public static Singleton getInstance() {
return instance;
}
}
这是最简单的实现,没有考虑空间效率和线程安全。
考虑空间效率
上面是饿汉式的实现过程,顾名思义,这个类太贪婪了,刚声明就迫不及待把内存空间开辟后据为己有了,不管此时它是不是立刻就需要。那么如何让它“懒”一点,到需要用的时候再开辟空间呢?实现过程如下:
//懒汉式
private static Singleton instance = null;
public static Singleton getInstance() {
if(null == instance) instance = new Singleton();
return instance;
}
new Singleton()从声明变量处移到了getInstance()函数里。这样在程序刚开始运行时,创建的只是空指针instance,只有当外界第一次调用getInstance()时才真的去创建它。这里是考虑了节约空间资源,但是又有了一个新问题。
考虑线程安全
当创建饿汉式单例模式时并不会遇到这个问题,因为类的初始化是不会发生线程安全问题的。但是当我们把对象的初始化放到一个函数中时就可能出现问题。试想,一个线程查看到该对象没有被创建,于是准备着手创建,此时另一个线程也发现该对象没有被创建,于是巧了,它俩想一块去了,都各自创建了一遍,此时内存中就存在两个对象,显然不是我们想看到的。解决这个问题,可通过添加synchronized关键字锁住资源,防止写冲突。
//改进后线程安全的双重锁懒汉式
private volatile static Singleton instance = null; //volatile 禁止重排序,所有的写操作将在读操作以前
public static Singleton getInstance() {
if(null == instance) {
synchronized (Singleton.class) {
if(null == instance) instance = new Singleton();
}
}
return instance;
}
volatile关键字的使用很有讲究,参考Java中的双重检查锁(double checked locking),里面有更为准确的介绍。但是这个依然是不安全的,于是有了下面这个终极答案:
//内部静态类实现
public static Singleton getInstance() {
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
很精妙,只能说Java博大精深。
测试
下面进入测试环节。我们知道,显然不能通过new来创建实例,那么正确的代码应该写作如下格式:
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
System.out.println("创建s1: " + s1.toString());
Singleton s2 = Singleton.getInstance();
System.out.println("创建s2: " + s2.toString());
}
这里我获取了两次实例对象,来看看到底是不是同一个。加上私有构造函数中同样打印输出了对象的哈希码(见定义部分),一共应该输出三次才对。四个实现代码的运行过程如下:




我还以为哈希码是不会一样的,这样显得好像是同一张图hhh。总之,每次运行结果都可以看出,无论获取多少次实例对象,都只会指向同一个,就是第一次构造函数创建的那一个。
以上就是这一次关于单例模式的小总结。写的过程中才发现还有很多东西没搞透。如有不严谨的地方敬请指出。
本文介绍了单例模式的定义、空间效率和线程安全的考虑,以及提供了Java中不同实现方式的代码示例,并进行了详细的测试,确保单例模式的正确性。在考虑空间效率时,采用了懒汉式实现,而在处理线程安全问题时,运用了双重检查锁定机制。测试结果显示,无论获取多少次,单例实例始终相同。

1747

被折叠的 条评论
为什么被折叠?



