一、单件模式定义及经典实现
确保一个类只有一个实例,并提供一个全局访问点。
单例的经典(简单)实现如下:
public class Singleton{
// 利用静态变量记录Singleton类的唯一实例
private static Singleton uniqueInstance;
// ... 类的其他成员和方法
// 声明构造器为私有,即只有本类内才可以调用(new Singleton())
private Singleton() {}
// 全局访问点:只有通过该类的getInstance方法才能获取到唯一实例
public static Singleton getInstance() {
if (uniqueInstance == null) {
// 如果实例不存在,则创建实例,以此确保只有一个唯一实例
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
因为getInstance是static方法,所以可以通过类名直接调用:Singleton.getInstance,这也是获取唯一实例的唯一接口。
总的说来,单例模式要素有:
- 私有构造方法
- 私有静态引用指向自己实例
- 以自己实例为返回值的公有静态方法
二、多线程问题及解决方法
多线程问题
针对上述简单实现,如果有多个线程同时调用getInstance方法,那么就有可能创建出多个“单例”:A、B线程都判断到uniqueInstance==null,因而各自都创建了一个实例。
解决办法
通常有以下几个解决办法,可以根据自身使用场景和程序特点来决定使用哪一个。
1. 同步(synchronized)getInstance方法
public class Singleton{
private static Singleton uniqueInstance;
// ... 类的其他成员和方法
private Singleton() {}
// 全局访问点:只有通过该类的getInstance方法才能获取到唯一实例
public static synchronized Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
这种方式简单粗暴,但降低了程序的性能:通常同步一个方法可能造成程序执行效率下降100倍!因此,这种方式只适合于getInstance调用频率很低的场合,或是程序可以接受这种同步方法所带来的性能影响。
2. JVM类加载时就创建单例
public class Singleton{
// 在静态初始化器中实例化单件
private static Singleton uniqueInstance = new Singleton();
// ... 类的其他成员和方法
private Singleton() {}
// 全局访问点:只有通过该类的getInstance方法才能获取到唯一实例
public static Singleton getInstance() {
// 已经有实例了,就直接返回
return uniqueInstance;
}
}
优点: JVM可以保证在任何线程访问uniqueInstance之前,一定先创建实例。因而不存在多线程问题。
缺点: 如果实例化这个单件是一个非常耗费资源和时间的工作,而这个单件可能在很后面的地方才被使用(甚至可能不被使用),那么这种“急切”实例化的方式将影响程序的运行体验。
3. 双重检查加锁,减少锁的使用次数
public class Singleton{
// 在静态初始化器中实例化单件
private volatile static Singleton uniqueInstance = new Singleton();
// ... 类的其他成员和方法
private Singleton() {}
// 全局访问点:只有通过该类的getInstance方法才能获取到唯一实例
public static Singleton getInstance() {
// 实例不存在,进入加锁的代码块;因此只有第一次(实例不存在),才进入加锁块(只是用一次锁)
if (uniqueInstance == null) {
synchronized (Singleton.class) {
// 再次检查
if (uniqueInstance == null){
uniqueInstance = new Singlton();
}
}
}
return uniqueInstance;
}
}
volatile的作用:确保当uniqueInstance变量被初始化成Singleton实例时,多个线程都能及时知道。用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。
volatile的原理: JVM中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。如果主内存中变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化。对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
三、单件模式优缺点
优点
- 在单例模式中,活动的单例只有一个实例,提供了对唯一实例的受控访问。这样可防止其它对象对自己的实例化,确保所有的对象都访问一个实例 。
- 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁同样对象时单例模式无疑可以提高系统的性能。
- 避免对共享资源的多重占用。
缺点
- 单件模式不适用于变化的对象。因为单例模式本质上就是一个全局变量,是程序中很大的一个污染源,因而会存在全局变量固有的缺点:在多线程环境下不安全。所以单件类最好是存储一些不变的东西。
- 有的单例使用私有化构造函数,使其不能被继承。如果你的程序中要对单例进行继承,请检查你是否错误地使用了单例模式。
- 单例类既要完成自己的本身业务职能,又要管理自己的实例化,在一定程度上违背了“单一职责原则”(但大家用习惯了,也就ok了)。
- 两个类加载器可能各自创建自己的单例实现:每个类加载器都定义了一个命名空间,不同的加载器加载同一个单件类时,就可能产生多个单例。
四、单件模式使用场景
单例模式具有节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如:
- Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
- windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
- 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
- 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
五、单例vs普通全局变量
- 全局变量能够提供全局访问,但不能确保只有一个实例。
- 全局变量属于急切实例化类型,而单例可以延迟实例化。