单例设计模式:保证某个类在所有的软件环境中只存在一个对象实例。这就是为什么它被叫做单例。
在开始解释什么是单例设计模式之前,现来看一下它的实现方式:
实现单例模式的方式
- 饿汉式(静态常量)
- 饿汉式(静态代码块)
- 懒汉式(线程不安全)
- 懒汉式(线程安全,同步方法)
- 懒汉式(线程安全,同步代码块)
- 双重检查
- 静态内部类
-
静态常量实现
步骤:
私有化constructor
- 类的内部创建对象
- 暴露静态公共方法,例如:getInstance
- 实现上述方法
其中1,2意味着外部不能新建对象且仅能从内部直接访问该对象。3意味着这个方法调用只能返回同一个实例。
class Singleton{
// Step 1
private Singleton(){}
// Step 2
private final static Singleton singleton = new Singleton();
// Step 3 ,4
public static Singleton getInstance(){return this.singleton;}
}
Pros and Cons
- Pros
- 写法简单,在类装载时实例化,线程安全
- Cons
- 因为不是懒加载,如果使用频率不高,则会造成资源浪费
在类装载时实例化意味着如果不能确定类装载的时机,那么getInstance可能不是唯一能够装载该类返回实例的方法。
- 因为不是懒加载,如果使用频率不高,则会造成资源浪费
静态代码块实现
和上一种实现方式相仿:
class Singleton{
private Singleton(){}
private static Singleton singleton;
static {
singleton = new Singleton;
}
public static Singleton getInstance(){return this.singleton;}
}
静态代码块实现__Singleton
应当注意的是,除了实现方式上略有区别,但静态代码块仍然同静态变量一样的优势和缺点。
懒汉式
线程不安全
为了解决单例不用就会造成资源浪费的问题,采用懒加载的策略可以对这一点进行优化,但要注意线程安全问题。
class Singleton{
private Singleton(){}
private static Singleton singleton;
public static Singleton getInstance(){
if(singleton == null) this.singleton = new Singleton(); // Lazy load
return singleton;
}
}
虽然看起来在getInstance时进行了判断,也确实起到了Lazy loading的效果,但是,同样由于进行了判断,在多个线程进行到if语句时,有可能会破坏单例模式的原则:只存在一个对象实例。
线程安全
在Java中,使用 synchronized
修饰 getInstance
方法可以保证只有一个线程可以访问到这一代码片段。
Easy and simple,right?需要注意,这样就表明了这是一个同步的方法。
同时,这里用 synchronized
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。这一步骤给每一个想要调用该方法的线程都造成了效率损失,因此同步的线程安全法很多时候不符合需求。
synchronized是一种独占式的重量级锁,在运行到同步方法或者同步代码块的时候,让程序的运行级别由用户态切换到内核态,把所有的线程挂起,通过操作系统的指令,去调度线程。这样会频繁出现程序运行状态的切换,线程的挂起和唤醒,会消耗系统资源,为了提高效率,引入了偏向锁、轻量级锁、尽量让多线程访问公共资源的时候,不进行程序运行状态的切换。
此外,除了修饰静态方法,synchronize也可以修饰代码块,因此在实现getInsance时可以仅将创建对象的那部分修饰,然而由于if语句的关系,此时又是线程不安全的(尽管效率有可能比修饰静态方法的要高一点点,因为在修饰代码块时是指令级的),不推荐使用此方式。
Double Check
解决线程安全和运行效率,同时也是lazy loading。
class Singleton{
private Singleton(){}
private static volatile Singleton singleton;
public static Singleton getInstance(){
if(singleton == null) {
synchronized (Singleton.class){
if(singleton == null) singleton = new Singleton();
}
}
return singleton;
}
}
volatile
确保了变量的修改能被立即同步。
getInstance中使用了 synchronized
修饰代码段确保了线程安全。同时,在多线程情况下,由于方法并未声明同步,未持有锁的线程会在进行到第一个if条件时自旋/阻塞,而只有持有锁的线程才能继续判断第二个if语句并修改变量。
这里之所欲使用double check,就是因为只有修改变量是同步的,而且依靠 volatile
修饰的这个变量也会在新持有锁的线程进入临界区之前发生改变(被上一个线程改的),所以需要增加check。
而当已存在单例(也就是那个singleton)的时候,在第一个if条件就会返回,不需要额外的等待。
这里我隐约觉着其实和Java的锁的等级机制有些相仿,第一个if就是偏向锁,第二个if则是轻量级自旋锁。
以静态内部类实现Singleton
class Singleton{
private Singleton(){}
// 静态内部类不会随着外部类的装载而装载
// 当被调用时(在getInstance中)静态内部类才会以线程安全的方式加载
private static class SingletonInstance{
private static final Singleton SINGLETON = new Singleton();
}
public static synchronized Singleton getInstance(){
return SingletonInstance.SINGLETON;
}
}
据上面的代码可以得出,静态内部类实现Singleton是线程安全的,且是lazy loading的。
以枚举实现Singelton
enum Singleton{
SINGLETON_1;
public String foo(){return "foo";}
}
枚举结构,单例模式,门当户对。
线程安全,且不会因反序列化重新创建对象。
Effective Java 作者 Josh Bloch推荐使用
JDK中的Singleton
注意事项
虽然实现单例的方式可能有很多,但是针对不同语言的实现方式和实用程度并不相同。在某些动态类型语言中,实现单例更多地需要参考语言特性。比如,在python中实现单例:
class Singleton(object):
def __new__(cls, *args, **kw):
if not hasattr(cls, '_instance'):
cls._instance = super().__new__(cls)
return cls._instance
class Foo(Singleton):
x = 99
a = Foo()
b = Foo()
id(a) == id(b)?print("Is singleton"):print("failed")
这种方式本质上类似于静态常量,只是因为python的属性没有实际的访问限制,所以在new处就要增加check,当然,这种方式存在着一些问题,我们已经讨论到了。
下面是使用装饰器的实现:
class Singleton:
instance = {}
def __init__(self, cls):
self.cls = cls
def __call__(self, *args, **kw):
if self.cls not in self.instance:
self.instance[self.cls] = self.cls(*args, *kw)
return self.instance[self.cls]
@Singleton
class Foo:
def __init__(self):
pass
当然,把某个类写成模块然后被其他模块导入也能起到单例的效果。
使用单例时往往会增加耦合度 。