第六章 单例模式
为什么需要单例模式?
有些对象只需要一个,例如:线程池、缓存和日志对象。
单例模式的特点:
- 保证一个类仅有一个实例,并提供一个访问它的全局访问点;
- 防止一个全局使用的类频繁地创建与销毁;
- 判断系统是否已经有这个单例,如果有则返回,如果没有则创建;
- 构造函数是私有的,若提供公共的构造函数 ,则可以构造多个该类对象。
1.1 饿汉式单例
在JVM启动便加载,优点是代码简单,缺点是存在启动时的性能消耗、若未使用该对象,会浪费资源等问题。
package headfirst.designpatterns.singleton.hungryLoding;
public class Singleton {
// 类加载时便初始单例对象
private static Singleton uniqueInstance = new Singleton();
private Singleton() {
}
// 提供全局访问点,由于私有构造,所有方法声明为static(类方法),可直接调用,不用new对象。
public static Singleton getInstance(){
return uniqueInstance;
}
}
1.2 同步方法单例
使用synchronized关键字修饰全局访问的方法,可保证在多线程的环境下只有一个线程访问。
package headfirst.designpatterns.singleton.syncMethod;
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
// 饿汉式加载,当第一次使用到该对象时,才创建该对象,
// 使用synchronized保证创建或访问单例对象时,只有一个线程获取到锁,避免造成线程不安全。
public static synchronized Singleton getInstance() {
if (uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
1.3 双重校验锁方法单例
synchronized 方法修饰方法,会有较大的性能损耗,所以提出了双重校验锁的方法进一步优化。
package headfirst.designpatterns.singleton.doubleCheckedLocking;
public class Singleton {
// 使用 volatile 关键字是为了防止在多线程的环境下,防止在创建单例对象时重排指令,进而造成线程不安全。
private static volatile Singleton uniqueInstance;
private Singleton() {
}
// 由于没有使用 synchronized 修饰方法,所以多个线程可能同时进入该方法
public static Singleton getInstance(){
if (uniqueInstance == null){
// 若线程1、2同时进入这里,线程1拿到锁进入。
// ...线程2等待线程1释放锁,线程2拿到锁后,由于线程1已经创建了单例对象,
// 所以线程2无需再次创建单例对象,故在进入同步代码块后,需再次判断单例对象是否为空。
synchronized(Singleton.class){
if (uniqueInstance == null){
uniqueInstance = new Singleton();//线程1拿到锁返回
}
}
}
return uniqueInstance;
}
}
关于为什么使用volatile关键字修饰单例对象变量?
uniqueInstance = new Singleton();
这行代码是由三个步骤组成,
- 1.分配对象内存,
- 2.初始化单例对象
- 3.引用变量指向单例对象
如下图,如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。
上图参考博客:https://blog.youkuaiyun.com/wanghao112956/article/details/99672270
参考
[1] Freeman E. Head First 设计模式[M] 中国电力出版社.
[2] 菜鸟教程.