Markdown版本笔记 | 我的GitHub首页 | 我的博客 | 我的微信 | 我的邮箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
Singleton 单例模式 设计模式 Single
目录
简介
五种方式
饿汉式(不推荐)
懒汉式(常用)
懒汉加锁方式
对 getInstance 方法加锁
Double Check Lock 模式
1.6 以后的 DCL 模式
静态内部类方式(推荐)
枚举式(简单)
简介
作用:保证类只有一个实例;提供一个全局访问点
JDK中的案例:
java.lang.Runtime#getRuntime()
五种方式
饿汉式(不推荐)
第一次加载类的时候会连带着创建 SINGLETON 实例
- 优点:以空间换时间。因为静态变量会在类装载时初始化,此时不会涉及多个线程对象访问该对象的问题,虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题,因此,可以省略
synchronized
关键字。 - 问题:如果只是加载本类,而不是调用
getInstance()
,甚至永远没有调用,则会造成资源浪费
!
class Single {
private static Single SINGLETON = new Single();//类加载后,对象就已经存在了
private Single() {
}
public static Single getInstance() {
return SINGLETON;
}
}
懒汉式(常用)
貌似大多数人都是用的懒汉式,核心是单例对象延时加载:
- 要点:以时间换空间。
lazy load
,延时加载,懒加载!真正用的时候才加载! - 问题:在多线程环境下存在风险
public class Single {
private static Single SINGLETON;
private Single() {
}
public static Single getInstance() {
if (SINGLETON == null) SINGLETON = new Single();
return SINGLETON;
}
}
线程安全问题
- 如果两个线程A和B同时执行了该方法,然后以如下方式执行:
- A进入if判断,此时 instance 为 null,因此进入if内
- B进入if判断,此时A还没有创建 instance,因此 instance 也为 null,因此B也进入if内
- A创建了一个instance并返回
- B也创建了一个instance并返回
此时问题出现了,我们的单例被创建了两次!
懒汉加锁方式
对 getInstance 方法加锁
以上问题最直观的解决办法就是给getInstance方法加上一个synchronize
前缀,这样每次只允许一个线程调用getInstance方法:
public class Single {
private static Single SINGLETON;
private Single() {
}
public static synchronized Single getInstance() {
if (SINGLETON == null) SINGLETON = new Single();
return SINGLETON;
}
}
这种解决办法的确可以防止错误的出现,但是它却很影响性能:每次调用getInstance方法的时候都必须获得Singleton的锁,而实际上,当单例实例被创建以后,其后的请求没有必要再使用互斥机制了。
Double Check Lock 模式
目前大量人使用的都是这个double-checked locking
的解决方案:
public class Single {
private volatile static Single SINGLETON; //添加volatile关键字
private Single() {
}
public static Single getInstance() {
if (SINGLETON == null) { //避免非必要加锁
synchronized (Single.class) { //加锁
if (SINGLETON == null) SINGLETON = new Single();
}
}
return SINGLETON;
}
}
让我们来看一下这个代码是如何工作的:
首先当一个线程发出请求后,会先检查instance是否为null,如果不是则直接返回其内容,这样避免了进入synchronized块所需要花费的资源。其次,即使第2节提到的情况发生了,两个线程同时进入了第一个if判断,那么他们也必须按照顺序执行synchronized块中的代码,第一个进入代码块的线程会创建一个新的Singleton实例,而后续的线程则因为无法通过if判断,而不会创建多余的实例。
上述描述似乎已经解决了我们面临的所有问题,但实际上,从JVM的角度讲,这些代码仍然可能发生错误。
对于JVM而言,它执行的是一个个Java指令。在Java指令中创建对象和赋值操作是分开进行的
,也就是说instance = new Singleton();
语句是分两步执行的,但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例
。即先赋值指向了内存地址,再初始化,这样就使出错成为了可能。
我们仍然以A、B两个线程为例:
- A、B线程同时进入了第一个 if 判断
- A首先进入synchronized块,由于instance为null,所以它执行
instance = new Singleton();
- 由于JVM内部的优化机制,JVM先划出了一些分配给Singleton实例的
空白内存
,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。 - B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。
- 此时B线程打算使用Singleton实例,
却发现它没有被初始化
,于是错误发生了。
1.6 以后的 DCL 模式
在JDK1.5
之后,官方也发现了这个问题,故而具体化了 volatile
,即在JDK1.6
及以后,只要定义为private volatile static
就可解决DCL失效问题。
volatile 确保INSTANCE每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。
静态内部类方式(推荐)
要点:不会像饿汉式那样立即加载对象,只有调用 getInstance() 才会加载静态内部类,且加载类时是线程安全的
,从而保证了并发高效调用和延迟加载
的优势!
JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的
。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。
此外该方法也只会在第一次调用的时候使用互斥机制,这样就解决了上面的低效问题。
最后instance是在第一次加载SingletonContainer类时被创建的,而SingletonContainer类则在调用getInstance方法的时候才会被加载,因此也实现了惰性加载。
public class Single {
private Single() {
}
public static Single getInstance() { //只有调用 getInstance() 才会加载静态内部类
return SingleHolder.INSTANCE;
}
private static class SingleHolder {
private static final Single INSTANCE = new Single();
}
}
这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
枚举式(简单)
线程安全、调用效率高,但不能延时加载,并且可以天然的防止反射和反序列化漏洞!
public enum Single {
SINGLETON;//定义一个枚举的元素,它就代表了Single的一个实例。元素的名字随意。
private String name;
Single() {
name = "初始化";
}
public String getName() {
return name;
}
}
2017-11-27