单例模式

来源:互联网 发布:手机淘宝主图什么格式 编辑:程序博客网 时间:2024/05/29 19:41

定义

单例模式顾名思义,在全局范围内提供唯一的实例对象供全局访问,一般用于全局的资源监控或者特定数量的资源访问控制(引申为特定数量的单例对象而非唯一,此处以单个实例对象为例),常用的示例如系统日志、单个打印机等对象,避免混乱发生错误,所以维持全局只有一个对象。

结构如下


单例模式作为对象创建性模式,所关心的只有两个点:

1、只有一个实例对象被创建

2、实例的创建时间可控制

结构

根据实例对象的创建时间,可以分为两种结构:

饿汉单例

在类加载时即创建实例对象,无须考虑后续并发环境对实例创建可能造成的影响(因为已经创建完成了,说这句废话是为了与懒汉单例对比),当然在实际使用单例对象时仍然需要加以同步控制,这是无论是饿汉或者懒汉都无法避免的。

参考代码

public class t{public static void main(String[] args){Singleton s1=Singleton.getInstance();Singleton s2=Singleton.getInstance();System.out.println(s1==s2);}}class Singleton{private static Singleton instance=new Singleton();private Singleton(){}public static Singleton getInstance(){return instance;}}
参考上述代码可知,实例对象s1与s2表示同一个对象。使用该形式单例的一个缺陷是,不能控制对象创建的时间,即在类加载执行实例的创建工作,而无论当前是否需要使用实例对象,可能会存在资源浪费。如下

public class t{public static void main(String[] args){Singleton.test();}}class Singleton{private static Singleton instance=null;static{instance=new Singleton();System.out.println("instance initialized");}private Singleton(){}public static Singleton getInstance(){return instance;}public static void test(){System.out.println("just for test");}}
输出

E:\>java tinstance initializedjust for test

懒汉单例

懒汉单例相对饿汉单例则是在需要使用单例对象时才进行实例创建工作,因为“懒”,所以能够实现所谓的延迟加载。但是也是因为“懒”,所以需要提供并发环境下的“安全”对象创建的保障。

这里所谓的“安全”,有两个方面需要考虑:

情形1:创建的实例对象多于一个

class Singleton{private static Singleton instance=null;private Singleton(){}public static Singleton getInstance(){if(instance==null){synchronized(Singleton.class){instance=new Singleton();}}return instance;}}
在高并发环境下完成初次单例对象创建工作时,可能会存在两个或多个线程同时判断instance==null,该情形下会顺序等待执行同步代码块,由于在进入synchronized同步块之后,没有进行实例是否存在的判断,所以结果可能会创建多个实例对象。

情形2:使用的是未完成创建的实例对象(残缺品)
情形1可能会创建多个单例对象,原因是在进入同步代码块后没有进行单例对象存在与否的判断,改进代码如下
class Singleton{private static Singleton instance=null;private Singleton(){}public static Singleton getInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=new Singleton();}}}return instance;}}
就是在进入同步块后再次进行判断,该情形下当多个线程判断instance==null,则顺序等待进入同步代码块,当第二个线程进入同步块,判断instance!=null,则可以直接使用第一个线程已经“创建”完成的单例对象,甚至于所有新来的线程当判断instance!=null时,不需等待同步代码块,直接使用已经“创建”完成的单例对象。
真实的情况可能跟表面呈现出的不一致,这里造成不一致的原因就是指令重排序,指令重排序本身是一种编译器或处理器的优化策略,在保证结果正确的前提下对中间的指令进行优化处理,最简单情形如下:
a=0;b=0;Action set{  a=1;  b=2;}Action get{  if(b==2){    return a;  }  return -1;}
线程1执行set,线程2执行get,因为a、b的赋值之间没有依赖关系,所以可以进行指令重排序,因此线程2中,当b值为2时,a的值不确定是否已经赋值为1(关于依赖关系,具体见JVM内存模型有序性)。
关于单例对象的创建简写为以下三个步骤:
1、分配对象空间
2、对象初始化
3、将对象地址赋值给instance引用变量
此处可能存在的问题就是,在步骤2尚未完成,步骤3已经完成,即表面上已经完成实例对象的“创建”,即instance!=null。
所以此处需要改进为
class Singleton{private static volatile Singleton instance=null;private Singleton(){}public static Singleton getInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=new Singleton();}}}return instance;}}
只是在instance引用变量上加上volatile关键字,volatile提供的有两个功能:
1、内存可见性
2、作为内存屏障,即禁止指令重排序
(volatile介绍太普遍,此处只强调一点,并不能保证原子性,可以保证可见性、有序性)
此处依赖的就是其内存屏障的作用(该方式也就是传说中的双重检查锁定DCL)。
不过观察该方式,仍存在的问题是,synchronized关键字保证同步性自然是不可少的,但是volatile作为一种轻量级同步(它不能保证同步,前两句已说其不能保证原子性),限制了一些优化措施。所以有一种利用静态内部类的实现形式如下
class Singleton{private Singleton(){}private static class Holder{final static Singleton instance=new Singleton();}public static Singleton getInstance(){return Holder.instance;}}
静态内部类在调用时才进行加载,并且加载机制可以保证线程安全性,所以此处将synchronized和volatile提供的同步访问和有序性等安全保障,都转移到静态内部类的加载机制中完成。

总结

单例模式作为向全局提供单一实例访问的模式,有两种实例对象构造方式:饿汉单例和懒汉单例,关键就在与对象的创建时间的差异,因为懒汉单例并不是在类加载是完成实例创建,所以需要提供安全性保障,避免实例对象不单一或者不完整的情况。



0 0
原创粉丝点击