单例模式属于创建型模式,是Java中最简单的设计模式之一。这种模式负责创建自己的对象,并且确保只创建一个对象,通过提供一种访问其唯一对象的方式,直接访问实例,不需要再实例化该类对象
解决问题:避免一个全局使用的类,频繁地创建与销毁
设计思路:创建一个SingleTon类,该类的构造方法设置为私有化,提供一个静态方法给外部访问
如何保证对象唯一性:
1.不允许其它程序用new创建该对象
2.在该类创建一个本类实例。
3.对外提供一个方法让其他程序可以获取该类对象。
步骤:
1.私有化该类构造函数。
2.通过new在本类中创建一个本类对象。
3.定义一个公有的方法,将创建的对象返回。
实现方式
单例的几种常用实现方式
懒汉式(不推荐)
懒汉顾名思义,在使用时才开始创建实例对象,不常用。懒汉式有线程不安全和线程安全两种实现方式
线程不安全。这种方式是最基础的实现方式,但有一个最大的问题,即不支持多线程,在多线程情况下不能正常工作。代码实现
public class Singleton {
// 创建 Singleton 实例对象
private static Singleton singleton;
private Singleton() {
}
// 获取唯一可用对象
public static Singleton getInstance() {
if (singleton==null){
singleton=new Singleton();
}
return singleton;
}
public void see() {
System.out.println("see best scene...");
}
}
线程安全。如过果希望线程安全,我们可以给上述代码加一个锁。与线程不安全的实现一样,第一次调用时才初始化,避免内存浪费。但是加锁会导致效率低,99%的情况下是不需要同步的
饿汉式(推荐)
饿汉在使用之前,已经提前初始化好实例。它通过classloader机制来保证多线程安全的,不需要加锁,但由于在类加载时就实例化,浪费内存。比较常用。
public class Singleton {
// 创建 Singleton 实例对象
private static Singleton singleton=new Singleton();
private Singleton() {
}
public static Singleton getInstance(){
return singleton;
}
}
双重校验锁(特殊需求考虑)
双锁机制核心思想,是先判断对象是否已经被初始化,再决定要不要加锁。通过双锁机制能保证安全且在多线程情况下保持高性能。实现难度相对复杂。
在懒汉式线程安全实现中,通过 synchronized 关键字实现加锁的操作,能保证线程安全,并且在每一次调用时都会进行加锁操作(其实加锁只需要在第一次初始化时用到,后续调用都没必要加锁),会导致很大性能开销。
执行双重检查时,如果多个线程同时通过了第一次检查,并且其中一个线程首先通过了第二次检查并且实例化了对象,那剩余的通过了第一次检查的线程就不会再去实例化对象。这样,除了初始化时会出现加锁,只要instance被创建之后,再调用getInstance()函数都不会进入到加锁逻辑中,后续所有调用都会避免加锁而直接返回,解决了性能的消耗问题
public class Singleton {
// 创建 Singleton 实例对象
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {// 检查变量是否初始化,不去获取锁,如果已初始化直接返回
synchronized (Singleton.class) {// 获取类锁
if (singleton == null) {// 再次判断变量是否初始化,如果未初始化就初始化一个对象
singleton = new Singleton();// 实例化对象 Singleton
}
}
}
return singleton;
}
}
这里存在一个隐患,多线程场景下可能导致访问未初始化对象。看下实例化对象的那行代码(标记为"//实例化对象 Singleton"的那行),实际上可以分解成以下三个步骤:
-
分配内存空间
-
初始化对象
-
将对象指向刚分配的内存空间
但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:
-
分配内存空间
-
将对象指向刚分配的内存空间
-
初始化对象
现在考虑重排序后,两个线程发生了以下调用:
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到singleton为空 | |
T2 | 获取锁 | |
T3 | 再次检查到singleton为空 | |
T4 | 为singleton分配内存空间 | |
T5 | 将singleton指向内存空间 | |
T6 | 检查到singleton不为空 | |
T7 | 访问singleton(此时对象还未完成初始化) | |
T8 | 初始化singleton |
在这种情况下,T7时刻线程B对uniqueSingleton的访问,访问的是一个初始化未完成的对象。
怎么解决呢?通过volatile关键字,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
使用volatile关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。
下面代码只是增加了volatile ,就完美的实现了双锁检查机制
public class Singleton {
// 创建 Singleton 实例对象
private volatile static Singleton singleton; // volatile 防止重排序
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {// 检查变量是否初始化,不去获取锁,如果已初始化直接返回
synchronized (Singleton.class) {// 获取类锁
if (singleton == null) {// 再次判断变量是否初始化,如果未初始化就初始化一个对象
singleton = new Singleton();//
}
}
}
return singleton;
}
}
登记式/静态内部类
这种方式能达到双检锁方式一样的功效,但实现更简单。利用Java的静态内部类,当外部类Singleton被加载的时候,并不会创建SingletonHolder实例对象。只有当调用getInstance()方法时,SingletonHolder才会被加载,这个时候才会创建instance。instance的唯一性、创建过程的线程安全性,都由JVM来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
附录: