单例模式总结

来源:互联网 发布:万达影城知乎 编辑:程序博客网 时间:2024/06/06 00:21
单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问他的全局访问点。

通常我们可以让一个全局变量使得一个对象访问,但他不能防止你实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且他可以提供一个访问该实例的方法。
举例:每台计算机可以有若干个打印机,但是只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每个计算机可以有若干个通信端口,系统应当几种管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免整出多头情况。

单例模式优点:

1、单例模式可以保证唯一的实例。 

2、单例模式因为Singleton类封装它的唯一实例,这样它可以严格控制客户怎样访问它以及何时访问它。简单说就是对唯一实例的受控访问。

[图片]
Singleton类,定义一个getInstance操作,允许客户访问它定位唯一实例。getInstance是一个静态方法,主要负责创建自己的唯一实例。

一、懒汉式加载(延迟加载):在第一次调用的时实例化自己。
public class Singleton{
private  static Singleton singleton;
/**构造方法让其private,这就堵死了外界利用new创建此类实例的可能,同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问.
*/
private  Singleton(){}
//此方法是获得本类实例的唯一全局访问点
public static Singleton getInstance(){
if(singleton== null){//若实例不存在,则new一个新实例,否则返回已有的实例
singleton= new Singleton();
}
return singleton;

}
客户端代码:
public class Test{
public void static main(String[] args){
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
if(s1 == s2){
System.out.println("两个对象是相同的实例");
}
}
}

注意:
1.构造方法让其private,这就堵死了外界利用new创建此类实例的可能,同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问.
2.事实上,通过java反射机制是能够实例化构造方法为private的类的,那基本上会导致所有的Java单利实现失效。此处不做考虑。
3.多线程的程序中,多个线程同时,注意是同时访问Singleton类,调用getInstance()方法,会有可能造成创建多个实例的。所有上面懒汉式单例模式线程不安全,要实现线程安全,有以下四种方式,都是对getInstance这个方法改造。

二、同步延迟加载,在getInstance方法上加同步
public class Singleton{
private  static Singleton singleton;
private  Singleton(){}
public static synchronized Singleton getInstance(){
if(singleton== null){
singleton= new Singleton();
}
return singleton
}
}
在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的,

三、双重检查锁定
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的(可以防止一部分,不能完全),同时避免了每次都同步的性能损耗。

存在问题:

但是由于java平台的内存模型(无序写入)导致双重检查(double check)不是线程安全的,如果要用上面的这种方式,JDK5.0以后版本若使用volatile关键字可行。

问题产生的过程举例:

假设没有关键字volatile的情况下,两个线程A、B,都是第一次调用该单例方法,线程A先执行singleton= new Instance(),该构造方法是一个非原子操作,编译后生成多条字节码指令,由于JAVA的指令重排序,可能会先执行singleton的赋值操作,该操作实际只是在内存中开辟一片存储对象的区域后直接返回内存的引用,之后singleton便不为空了,但是实际的初始化操作却还没有执行,如果就在此时线程B进入,就会看到一个不为空的但是不完整(没有完成初始化)的singleton对象,所以需要加入volatile关键字,禁止指令重排序优化,从而安全的实现单例。

创建对象可以分解为如下的3行伪代码:
memory=allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance=memory; //3:设置instance指向刚分配的内存地址
上面3行代码中的2和3之间,可能会被重排序导致先3后2。由于 new Instance()是一个非原子操作,所以可能发生无序写入。

下面是加了volatile 线程安全的代码:
public class Singleton {
  private volatile static Singleton singleton = null;
 private Singleton() {}
  public static Singleton getInstance() {
  if (singleton== null) {
  synchronized (Singleton.class) {// 1
    if (singleton== null) {// 2
    singleton= new Singleton();// 3
    }
  }
  }
  return singleton ;
 }
}


原因:双重检测锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是失败的一个主要原因。
 
无序写入:
为解释该问题,需要重新考察上述清单中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量singleton来引用此对象。这行代码的问题是:在 Singleton 构造函数体执行之前,变量singleton可能成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程得到的是一个还会初始化的对象,这样会导致系统崩溃。
什么?这一说法可能让您始料未及,但事实确实如此。在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏的。假设代码执行以下事件序列:

1、线程 1 进入 getInstance() 方法。
2、由于 singleton 为 null,线程 1 在 //1 处进入 synchronized 块。 
3、线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非 null。 
4、线程 1 被线程 2 预占。
5、线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将 singleton 引用返回给一个构造完整但部分初始化了的 Singleton 对象。 
6、线程 2 被线程 1 预占。
7、线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。
 
为展示此事件的发生情况,假设代码行 singleton =new Singleton(); 执行了下列伪代码:
mem = allocate();             //为单例对象分配内存空间.
singleton= mem;               //注意,instance 引用现在是非空,但还未初始化
ctorSingleton(instance);    //为单例对象通过instance调用构造函数

这段伪代码不仅是可能的,而且是一些 JIT 编译器上真实发生的。执行的顺序是颠倒的,但鉴于当前的内存模型,这也是允许发生的。JIT 编译器的这一行为使双重检查锁定的问题只不过是一次学术实践而已。
 
如果真像这篇文章:http://dev.csdn.net/author/axman/4c46d233b388419e9d8b025a3c507b17.html所说那样的话,1.2或以后的版本就不会有问题了,但这个规则是JMM的规范吗?谁能够确认一下。
确实,在JAVA2(以jdk1.2开始)以前对于实例字段是直接在主储区读写的.所以当一个线程对resource进行分配空间,初始化和调用构造方法时,可能在其它线程中分配空间动作可见了,而初始化和调用构造方法还没有完成.但是从JAVA2以后,JMM发生了根本的改变,分配空间,初始化,调用构造方法只会在线程的工作存储区完成,在没有向主存储区复制赋值时,其它线程绝对不可能见到这个过程.而这个字段复制到主存区的过程,更不会有分配空间后没有初始化或没有调用构造方法的可能.在JAVA中,一切都是按引用的值复制的.向主存储区同步其实就是把线程工作存储区的这个已经构造好的对象有压缩堆地址值COPY给主存储区的那个变量.这个过程对于其它线程,要么是resource为null,要么是完整的对象.绝对不会把一个已经分配空间却没有构造好的对象让其它线程可见。
 
另一篇详细分析文章:http://www.iteye.com/topic/260515

四、静态内部类:为了做到真真的延迟加载,双重检测在Java中是行不通的,所以只能借助于另一类的类加载加延迟加载:
public class Singleton{
private static class LazyHolder{
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return lazyHolder.INSTANCE;
}
}
利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。

上面第四种方法比上面二、三都好些,既实现了线程安全,又避免了同步带来的性能影响。


五、使用ThreadLocal修复双重检测
借助于ThreadLocal,将临界资源(需要同步的资源)线程局部化,具体到本例就是将双重检测的第一层检测条件 if (singleton== null) 转换为了线程局部范围内来作。这里的ThreadLocal也只是用作标示而已,用来标示每个线程是否已访问过,如果访问过,则不再需要走同步块,这样就提高了一定的效率。但是ThreadLocal在1.4以前的版本都较慢,但这与volatile相比却是安全的。
public class Singleton {
 private static final ThreadLocal perThreadInstance = new ThreadLocal();
 private static Singleton singleton ;
 private Singleton() {}
 
 public static Singleton  getInstance() {
  if (perThreadInstance.get() == null){
   // 每个线程第一次都会调用
   createInstance();
  }
  return singleton;
 }

 private static  final void createInstance() {
  synchronized (Singleton.class) {
   if (singleton == null){
    singleton = new Singleton();
   }
  }
  perThreadInstance.set(perThreadInstance);
 }
}

六、饿汉式单例模式:在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生就是线程安全的。非延迟加载单例类
public class Singleton{
private Singleton(){}
private static final Singleton single = new Singleton();
public static Singleton getInstance(){
return single;
}
}

七、登记式单例(可忽略)
//类似Spring里面的方法,将类名注册,下次从里面直接获取。
public class Singleton3 {
    private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();
    static{
        Singleton3 single = new Singleton3();
        map.put(single.getClass().getName(), single);
    }
    //保护的默认构造子
    protected Singleton3(){}
    //静态工厂方法,返还此类惟一的实例
    public static Singleton3 getInstance(String name) {
        if(name == null) {
            name = Singleton3.class.getName();
            System.out.println("name == null"+"--->name="+name);
        }
        if(map.get(name) == null) {
            try {
                map.put(name, (Singleton3) Class.forName(name).newInstance());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return map.get(name);
    }
    //一个示意性的商业方法
    public String about() {    
        return "Hello, I am RegSingleton.";    
    }    
    public static void main(String[] args) {
        Singleton3 single3 = Singleton3.getInstance(null);
        System.out.println(single3.about());
    }
}

 登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。 

这里我对登记式单例标记了可忽略,我的理解来说,首先它用的比较少,另外其实内部实现还是用的饿汉式单例,因为其中的static方法块,它的单例在类被装载的时候就被实例化了。


八、应用
public class TestSingleton {
String name = null;

private TestSingleton() {}
private static volatile TestSingleton instance = null;
public static TestSingleton getInstance() {
           if (instance == null) {  
             synchronized (TestSingleton.class) {  
                if (instance == null) {  
                   instance = new TestSingleton(); 
                }  
             }  
           } 
           return instance;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public void printInfo() {
System.out.println("the name is " + name);
}
}
可以看到里面加了volatile关键字来声明单例对象,既然synchronized已经起到了多线程下原子性、有序性、可见性的作用,为什么还要加volatile呢?

总结:

懒汉式和饿汉式区别:
饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单利是已经存在的了。饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成。
懒汉就是只有当调用getInstance的时候,才会去初始化这个单例。即懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
线程安全:
饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题。
懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是二、三、四、五这四种。但实现在资源加载和性能方面有些区别。


参考:
http://www.iteye.com/topic/652440
http://blog.csdn.net/jason0539/article/details/23297037/

0 0
原创粉丝点击