单例模式(singleton)
这个设计模式主要目的的想在整个系统中只能出现一个类的实例.参考深入浅出单实例SINGLETON设计模式
简单实现
|
|
上面的代码有以下几种特点:
- 私有的构造函数,表明这个类是不可能被其他代码实例化.
- 在方法getInstance()第一次被调用时,实例化对象.
- Singleton.getInstance() 可以得到一个对象.
实际版本
线程同步
在简单版本中有一个重要的问题,就是多线程环境下可能会有多个线程同事进行(singleton == null)的判断,从而创建多个Singleton对象,而且还有内存泄漏的风险.所以我们要实现多线程互斥或同步.
|
|
这样把判断和创建实例的代码块给同步起来.但是这样子代价很大.因为我们只需要保证第一次创建的时候将线程同步,到后面线程读取实例的时候并不需要同步,如果都同步的话,那么将严重地影响性能.优化代码如下:
|
|
第一个if检查是否已经有一个 Singleton实例
如果没有实例,那么开始同步线程
第二个if用来double-check是否存在 Singleton实例
new的原子操作
singleton = new Singleton() 并非是一个原子操作,实际上在JVM中这句话大概做了下面 3 件事情.
1.给 singleton 分配内存
2.调用 Singleton 的构造函数来初始化成员变量,形成实例
3.将singleton对象指向分配的内存空间(执行完这步 singleton才是非null了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
对此,我们只需要把singleton声明成 volatile 就可以了.
|
|
使用 volatile 有两个功用:
1)这个变量不会在多个线程中存在复本,直接从内存读取。
2)这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。
但是,这个事情仅在Java 1.5版后有用,1.5版之前用这个变量也有问题,因为老版本的Java的内存模型是有缺陷的。
简化版本
下面的这个1.6版是老版《Effective Java》中推荐的方式。
|
|
优雅版本
|
|
通过EasySingleton.INSTANCE来访问,这比调用getInstance()方法简单多了。
默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。
这个版本基本上消除了绝大多数的问题。代码也非常简单,实在无法不用。这也是新版的《Effective Java》中推荐的模式。
其它问题
有些反例可能属于钻牛角尖,可能有点学院派,不过也不排除其实际可能性 :
Class Loader
这是Java动态性的核心。顾名思义,类装载器是用来把类(class)装载进JVM的。JVM规范定义了两种类型的类装载器:启动内装载器(bootstrap)和用户自定义装载器(user-defined class loader)。在一个JVM中可能存在多个ClassLoader,每个ClassLoader拥有自己的NameSpace。一个ClassLoader只能拥有一个class对象类型的实例,但是不同的ClassLoader可能拥有相同的class对象实例,这时可能产生致命的问题。如ClassLoaderA,装载了类A的类型实例A1,而ClassLoaderB,也装载了类A的对象实例A2。逻辑上讲A1=A2,但是由于A1和A2来自于不同的ClassLoader,它们实际上是完全不同的,如果A中定义了一个静态变量c,则c在不同的ClassLoader中的值是不同的。
于是,Singleton 2.1版本中多个实例同样会被多个Class Loader创建出来,当然,这个有点牵强,不过他确实存在。可是,我们怎么可能在我的Singleton类中操作Class Loader啊?是的,你根本不可能。在这种情况下,你能做的只有是——“保证多个Class Loader不会装载同一个Singleton”。
序例化
如果我们的这个Singleton类是一个关于我们程序配置信息的类。我们需要它有序列化的功能,那么,当反序列化的时候,我们将无法控制别人不多次反序列化。不过,我们可以利用一下Serializable接口的readResolve()方法,比如:
|
|
多个Java虚拟机
如果我们的程序运行在多个Java的虚拟机中。嗯,这种情况是有点极端,不过还是可能出现,比如EJB或RMI之流的东西。要在这种环境下避免多实例,看来只能通过良好的设计或非技术来解决了。
volatile变量
关于volatile这个关键字所声明的变量可以被看作是一种 “程度较轻的同步synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是synchronized的一部分。当然,如前面所述,我们需要的Singleton只是在创建的时候线程同步,而后面的读取则不需要同步。所以,volatile变量并不能帮助我们即能解决问题,又有好的性能。而且,这种变量只能在JDK 1.5+版后才能使用。
关于继承
是的,继承于Singleton后的子类也有可能造成多实例的问题。不过,因为我们早把Singleton的构造函数声明成了私有的,所以也就杜绝了继承这种事情。
关于代码重用
也话我们的系统中有很多个类需要用到这个模式,如果我们在每一个类都中有这样的代码,那么就显得有点傻了。那么,我们是否可以使用一种方法,把这具模式抽象出去?在C++下这是很容易的,因为有模板和友元,还支持栈上分配内存,所以比较容易一些(程序如下所示),Java下可能比较复杂一些.
|
|