• 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html
  • 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html

来看看你貌似熟悉的单例模式

技术杂谈 勤劳的小蚂蚁 2个月前 (02-09) 71次浏览 已收录 0个评论 扫描二维码

在众多的设计模式中,有几个模式大家都认为比较简单易用的,例如工厂模式单例模式等。
但正如唐诗所云:

最是平流无石处,时时闻说有沉沦。

今天我们就来看一看貌似了解的单例。

提起单例,相信许多人不以为然,单例么?不就是构造函数是private,在需要的时候getInstance就好了么。

道理没错,但问题的关键在于如何保证instance返回的是同一个对象!
所以,我们来谈谈单例的实现。

哎,客官,别急着走,也许后面的内容真有你不了解的内容呢,不信看看。

许多人都了解,单例的创建大致可以分为两种:
  • 饱汉型
  • 饿汉型

所谓饿汉型,就是单例的对象,我上来就先创建好了,什么时候用直接拿就好了。例如这样的方式:
private static final Singleton singleton = new Singleton();
public static Singleton getInstance() {
   return singleton;
}

这样的实现方式没有任何问题。问题出在哪呢?

有些时候,要实现Singleton的对象比较大,或者创建比较耗资源,耗时等,我们希望能在需要的时候再初始化,而不放在class 加载的时候,也就是实现所谓的lazy load。

这个时候问题就来了,这种所谓的饱汉型要怎么写呢?

你可能见到过这种形式的实现
private static Singleton singleton;
public static Singleton getInstance() {
   if (singleton == null) {
       singleton = new Singleton();
   }
   return singleton;
}

这样的实现,有问题么?是不是感觉棒棒的?

在单线程的环境中跑的话,这样也是可以保证只返回一个instance的。

问题出在多线程环境下执行。

多个线程执行时,极有可能两个线程同时执行到判断是否为null的情况,又同时创建了实例出来。为了解决多线程的问题,你毫不犹豫的给方法加上了synchronized,兵不血刃的解决了问题。

问题又来了!
当这个方法使用很频繁的时候,synchronized带来的互斥效果,导致每次只能一个线程执行,效率很低。
此时,一个聪明的想法浮现在脑海(当然,可能是查资料,网上浏览了解到的)。使用双重锁检查(Double lock checking)来提高效率,实现起来是这个样子:
public static Single getInstance() {
if(singleton == null) {
synchronized(this) {
if(singleton == null) {
singleton = new Singletoon();
}
}
}
return singleton;

}


我们的方法并不是互斥的,只有在instance为空时才会加锁检查。看似无懈可击!

这个时候有一个问题,是看似普通的new XXX这种操作,其本质上和i++
这种操作一样,并不是一个原子操作。例如,我们下面这几行代码:
public class Test {
private int i = 5;
private int a = 2;
public Test(int i, int a) {
this.i = i;
this.a = a;
}
public void ttt() {
      Test t = new Test(1,1); //普通的实例化一个对象
}
}
我们通过javap命令观察到,如此普通的实例化,其对应的jvm指令像下面图中所示

大致包含的步骤有:
  1. 创建对象
  2. 初始化对象的各个域,为其赋值
  3. 将对象指向其引用

但是,对于这些指令的执行,却并不一定是按照这个顺序执行,为了执行效率,这些指令会被优化,指令被重新排序。极有可能对象被创建后即指向了其引用,但各个域并没有初始化,如果此时被使用,那拿到的就是一个构造不完整的对象。(可以参考Java并发编程实战了解对象逃逸)

为了使代码不被优化影响,Java 5在修订了Java内存模型(JMM)之后,可以使用volatile声明,不允许指令重排序

volatile关键字同时保证了内存的有序性可见性,保证程序可以按照预期执行。所以,要实现一个正确无误的DCL单例,需要同时把singleton对象声明为
volatile,这一定很重要

如果不使用DCL,我们还有其它方式实现延迟初始化。例如下面这种内部类的形式,也是比较常用的。
public class Foo {
private static class FooHolder {
static final Foo foo = new Foo();
}
public static Foo getFoo() {
return FooHolder.foo;
}

}


由于内部类只有在使用时才会初始化,所以保证了单例的延迟初始化。

了解了以上这些后,我们来看Tomcat中的单例,是如何使用的。

首先我们来看看Tomcat中对于DCL的使用。
/** Whether the servlet needs reloading on next access */
private volatile boolean reload = true;

public Servlet getServlet() throws ServletException {
// DCL on ‘reload’ requires that ‘reload’ be volatile
   // (this also forces a read memory barrier, ensuring the
   // new servlet object is read consistently)
   if (reload) {
synchronized (this) {
// Synchronizing on jsw enables simultaneous loading
           // of different pages, but not the same page.
           if (reload) {
// This is to maintain the original protocol.
               destroy();
final Servlet servlet;
servlet.init(config);
reload = false;
// Volatile ‘reload’ forces in order write of ‘theServlet’ and new servlet object
           } } }
return theServlet;
}

上面的代码是关于jsp对应的Servlet获取时对应的代码,其中对于DCL的使用主要用于判断jsp文件对应的class是否需要重新加载。(jsp文件工作原理前面文章介绍过,感兴趣的朋友可以看JSP文件修改实时生效的秘密)

单例的使用,Tomcat中的方式很简单,
public final class ApplicationFilterFactory {

private static ApplicationFilterFactory factory = null;

private ApplicationFilterFactory() {
// Prevent instantiation outside of the getInstanceMethod().
   }
/**
* Return the factory instance.
*/
public static ApplicationFilterFactory getInstance() {
if (factory == null) {
factory = new ApplicationFilterFactory();
}
return factory;

}


我们看到和前面第一次提到的饱汉型一样,没有使用DCL,也没加
synchronized,这是因为在Tomcat中对于此处ApplicationFactory的使用,只有在StandardWrapperValve启动才会触发其初始化,并不会涉及到多线程环境的使用,所以可以放心使用这种方式。


看到这里的朋友,其实单例还有一种实现方式,是Effective Java的作者推荐使用的,使用起来更简单,只需要一个枚举项的enum即可,之后可以包含其对应的各个方法:
public enum Singleton {
INSTANCE;
public void test() {
System.out.println(“test”);
}
}

而我们使用的时候,直接这样使用即可:
Singleton.INSTANCE.test();


丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:来看看你貌似熟悉的单例模式
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

您必须 登录 才能发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00