1. 定义
单例模式:确保一个类只有一个实例,并提供一个全局访问点。
- 按照定义来看,也可以设置一个全局变量,同样能实现要求,但是全局变量却存在问题,如果将对象赋值给一个全局变量,那么必须在程序一开始就创建好对象,万一这个对象非常耗费资源,而程序在这次的执行过程中又一直没有用到它,就形成浪费了。
2. 用处
有一些对象只需要一个,例如配置文件,工具类,线程池,缓存,日志对象等等。单例模式保证应用中有且只有一个实例。
常用的单例模式
(1)懒汉式:指全局的单例实例在第一次被使用时构建。
(2)饿汉式:指全局的单例实例在类装载时构建。
日常我们使用的较多的应该是懒汉式的单例,毕竟按需加载才能做到资源的最大化利用。
3. 实现
原理:利用静态类变量、静态方法和适当的访问修饰符。
3.1. 经典的单例模式实现
/**
* 经典的单件模式实现
*/
public class Singleton01 {
// 利用一个静态变量来记录Singleton类的唯一实例
private static Singleton01 uniqueInstance;
/*
别的成员变量
*/
// 把构造器声明为私有的,只有从Singleton类内部才可以调用构造器
private Singleton01() {
}
//
public static Singleton01 getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton01();
}
return uniqueInstance;
}
/*
别的方法实现
*/
}
上面是单例模式最经典也是最简单的实现方法,但是当程序使用多线程的时候就会出现问题,如下图所示:
在上图中的程序就会出现有多个对象的情况,这是因为使用了多线程,处理该多线程灾难的一个方法是把getInstance()变成同步(synchronized)方法。
3.2. 处理多线程
经典单例遇到多线程就会创造出多于一个的实例,只要把getInstance()变成同步(synchronized)方法,多线程灾难就可以轻易地解决了。
/**
* 处理多线程:
* 经典单例遇到多线程就会创造出多于一个的实例
* 只要把getInstance()变成同步(synchronized)方法
* 多线程灾难就可以轻易地解决了
*/
public class Singleton02 {
// 利用一个静态变量来记录Singleton类的唯一实例
private static Singleton02 uniqueInstance;
/*
别的成员变量
*/
// 把构造器声明为私有的,只有从Singleton类内部才可以调用构造器
private Singleton02() {
}
//
public static synchronized Singleton02 getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton02();
}
return uniqueInstance;
}
/*
别的方法实现
*/
}
但是这样问题又来了,为了解决多线程的问题,我们引入了同步(synchronized)方法,但是同步会降低性能,这又是另一个问题。
3.3. 改善多线程
经典单例遇到多线程会创造出多于一个的实例,但是使用同步(synchronized)方法又会降低性能,因此可以进一步改善多线程。
(1)如果getInstance()的性能对应用程序不是很关键,就什么都别做
(2)使用“饿汉式”创建实例,而不延迟实例化
这里就用到我们的“饿汉式”了,即,全局的单例实例在类装载时构建。
如果应用程序总是创建并使用单例对象的实例,或者在创建和运行时方面的负担不太繁重,就可以使用“饿汉式”创建实例。
package npu.yyl.pattern.singleton;
/**
* 使用同步(synchronized)方法会降低性能
* 如果应用程序总是创建并使用单例对象的实例,
* 或者在创建和运行时方面的负担不太繁重,
* 就可以使用“饿汉式”创建实例
*/
public class Singleton03 {
// 在静态初始化器(static initializer)中创建单例
// 这段代码保证了线程安全(thread safe)
private static Singleton03 uniqueInstance = new Singleton03();
private Singleton03() {
}
//
public static Singleton03 getInstance() {
// 已经有实例了,直接使用它
return uniqueInstance;
}
/*
别的方法实现
*/
}
利用这个做法,我们依赖JVM在加载这个类时马上创建此唯一的单例实例。JVM保证在任何线程访问uniqueInstance静态变量之前,一定先创建此实例。
(3)用“双重加锁”,在getInstance()中减少使用同步
利用双重检查加锁(double-checked locking),首先检查是否实例已经创建了,如果尚未创建,“才”进行同步。这样一来,只有第一次会同步,这正是我们想要的。
package npu.yyl.pattern.singleton;
/**
* 利用双重检查加锁(double-checked locking),
* 首先检查是否实例已经创建了,
* 如果尚未创建,“才”进行同步。
* 这样一来,只有第一次会同步。
*/
public class Singleton04 {
// 利用一个静态变量来记录Singleton类的唯一实例
private volatile static Singleton04 uniqueInstance;
private Singleton04() {
}
public static synchronized Singleton04 getInstance() {
if (uniqueInstance == null) {
// 检查实例,如果不存在,就进入同步区块
synchronized (Singleton04.class) {
// 只有第一次才彻底执行这里的代码
if (uniqueInstance == null) {
// 进入区块后,再检查一次。如果仍是null,才创建实例
uniqueInstance = new Singleton04();
}
}
}
return uniqueInstance;
}
/*
别的方法实现
*/
}
volatile关键词确保,当uniqueInstance变量被初始化成Singleton实例时,多个线程正确的处理uniqueInstance变量。
这个方法关心了性能,大大减少了getInstance()的时间消耗。