单例设计模式
5.1 基本介绍
单例模式的定义就是确保某一个类只有一个实例,并且提供一个全局访问点。属于设计模式三大类中的创建型模式
。
单例模式具有典型的三个特点:
- 单例类只有一个实例对象
- 该单例对象必须由单例类自行创建
- 单例类对外提供一个访问该单例的全局访问点
5.2 单例设计模式的实现
举例:
我们可以做一个这样的尝试,在Windows的“任务栏”的右键弹出菜单上多次点击“启动任务管理器”,看能否打开多个任务管理器窗口?能找到咱两凑一对?
通常情况下,无论我们启动任务管理多少次,Windows系统始终只能弹出一个任务管理器窗口,也就是说在一个Windows系统中,任务管理器存在唯一性。
为什么要这样设计呢?我们可以从以下两个方面来分析:
其一,如果能弹出多个窗口,且这些窗口的内容完全一致,全部是重复对象,这势必会浪费系统资源,任务管理器需要获取系统运行时的诸多信息,这些信息的获取需要消耗一定的系统资源,包括CPU资源及内存资源等,浪费是可耻的,而且根本没有必要显示多个内容完全相同的窗口。
其二,如果弹出的多个窗口内容不一致,问题就更加严重了,这意味着在某一瞬间系统资源使用情况和进程、服务等信息存在多个状态,例如任务管理器窗口A显示“CPU使用率”为10%,窗口B显示“CPU使用率”为15%,到底哪个才是真实的呢?
5.2.1 饿汉式(立即加载)
实现步骤:
1、既然要保证类不能随便实例化,因此需要私有化构造器
private Singleton() {}
2、私有的静态的当前类的对象作为属性
private static final Singleton instance = new Singleton();
注意点:
-
private
如果不私有化属性,使用public会存在什么问题呢?
拿到的实例对象轻轻松松就给修改了,因此私有化属性,不让其进行修改。
-
static
如果没有使用static修饰会发生什么问题呢?
可以看出在未使用static修饰的话,出现了栈溢出错误。
使用static的话可以保证你的实例对象只有一份,不会出现溢出。
-
final
私有化属性还是存在问题,可以通过反射获取进行修改,因此使用final进行修饰,这下你通过反射对属性就无法进行修改了。
3、公有的静态的方法 返回当前唯一属性
// 当前类不能实例只能通过static进行方法获取
public static Singleton getInstance() {
return instance;
}
完整代码:
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
System.out.println(singleton1 == singleton2);
}
}
// 饿汉式(静态常量的方式)
class Singleton {
// 1 构造器私有化
private Singleton() {
}
// 2 本类内部创建对象实例
private static final Singleton instance = new Singleton();
// 3、共有的静态的方法 返回当前唯一属性
public static Singleton getInstance() {
return instance;
}
}
测试结果:
460141958
460141958
true
优缺点说明:
- 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
- 缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
- 结论:这种单例模式可用,可能造成内存浪费
5.2.2 懒汉式(延迟加载)
1、方式一(线程不安全)
代码实现:
class Singleton {
private static Singleton instance;
private Singleton() {}
// 提供一个静态的公有方法,当使用到该方法时,才去创建 instance
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优缺点说明:
- 起到了 Lazy Loading 的效果,但是只能在单线程下使用。
- 如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会 产生多个实例。所以在多线程环境下不可使用这种方式
- 结论:在实际开发中, 不要使用这种方式。
2、方式二(线程安全)
代码实现:
// 懒汉式(线程安全,同步方法)
class Singleton2 {
private static Singleton2 instance;
private Singleton2() {
}
//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
// 添加synchronized锁 解决线程安全的问题
public static synchronized Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
优缺点说明:
- 解决了 线程安全问题。
- 效率太低了,每个线程在想获得类的实例时候,执行 getInstance()方法都要进行同步。
- 结论:在实际开发中, 不推荐使用这种方式。
5.2.3 双重检查
代码实现:
// 懒汉式(线程安全,同步方法)
class Singleton {
// 添加volatile 保证属性 加载 赋值的过程中 不会被JVM指令重排序
private static volatile Singleton instance;
private Singleton() {}
//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
//同时保证了效率, 推荐使用
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
volatile:用大白话来讲就好比排队取号问题,你的位置已经固定,不允许穿插。
什么是双重检查加锁机制:
并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。
这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
注意在Java1.4及以前版本中,很多JVM对于volatile关键字的实现有问题,会导致双重检查加锁的失败,因此双重检查加锁的机制只能用在Java5及以上的版本。
这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是在第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。
提示:
由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就
是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用
优缺点说明:
- Double-Check 概念是多线程开发中常使用到的,如代码中所示,我们进行了两次 if (singleton == null)检查,这样就可以保证线程安全了。
- 实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接 return 实例化对象,也避免的反复进行方法同步。
- 线程安全,延迟加载,效率较高
- 结论:在实际开发中, 推荐使用这种单例设计模式(根据情况来选用)
5.2.4 静态内部类
代码实现:
//饿汉式(静态内部类)
class Singleton {
private Singleton() {}
// Singleton进行类装载的时候并不会创建,从而懒加载
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
// 调用时装载SingletonInstance,JVM装载时是线程安全的
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
优缺点说明:
- 这种方式采用了类装载的机制来保证初始化实例时只有一个线程
- 静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化。
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
- 优点: 避免了线程不安全,利用 静态内部类特点实现延迟加载,效率高
- 结论: 推荐使用
5.2.5 枚举
代码实现:
//使用枚举,可以实现单例, 推荐
public enum Singleton {
// 定义一个枚举元素,它就代表Singleton的一个实例
INSTANCE;
// 单例可以有自己的方法
public void singletonOperation() {
// 功能处理
}
}
优缺点说明:
- 这借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
- 结论: 推荐使用
5.2.6 单例模式在 JDK 应用的源码分析
我们 JDK 中,java.lang.Runtime 就是经典的单例模式(饿汉式)
5.2.7 单例模式的扩展
思考单例模式:
单例模式是为了控制在运行期间,某些类的实例数目只能有一个。
可能有人就会思考,能不能控制实例数目为2个,3个,或者是任意多个呢?
目的都是一样的,节约资源啊,有些时候单个实例不能满足实际的需要,会忙不过来,根据测算,3个实例刚刚好。
也就是说,现在要控制实例数目为3个,怎么办呢?
// 简单演示如何拓展单例模式,控制实例数目为3个
public class OneExtend {
// 定义一个缺省的key值的前缀
private final static String DEFAULT_PREKEY = "Cache";
// 缓存实例的容器
private static Map<String, OneExtend> map =
new HashMap<String, OneExtend>();
// 用来记录当前正在使用第几个实例,到了控制的最大数目,就返回从1开始
private static int num = 1;
// 定义控制实例的最大数目
private final static int NUM_MAX = 3;
// 构造器私有化
private OneExtend() {
}
public static OneExtend getInstance() {
String key = DEFAULT_PREKEY + num;
// 缓存的体现,通过控制缓存的数据多少来控制实例数目
OneExtend oneExtend = map.get(key);
if (oneExtend == null) {
oneExtend = new OneExtend();
map.put(key, oneExtend);
}
// 把当前实例序号加一
num++;
if (num > NUM_MAX) {
// 如果实例的序号达到最大值,从1开始
num = 1;
}
return oneExtend;
}
public static void main(String[] args) {
OneExtend t1 = getInstance();
OneExtend t2 = getInstance();
OneExtend t3 = getInstance();
OneExtend t4 = getInstance();
OneExtend t5 = getInstance();
OneExtend t6 = getInstance();
System.out.println("t1 = " + t1.hashCode());
System.out.println("t2 = " + t2.hashCode());
System.out.println("t3 = " + t3.hashCode());
System.out.println("t4 = " + t4.hashCode());
System.out.println("t5 = " + t5.hashCode());
System.out.println("t6 = " + t6.hashCode());
}
}
测试结果:
t1 = 460141958
t2 = 1163157884
t3 = 1956725890
t4 = 460141958
t5 = 1163157884
t6 = 1956725890