JAVA设计模式-辛格尔顿

概念

所谓单例模式。简单的说,这是为了确保在整个应用程序只有一个实例存在类。这就像Java Web该application。即,它提供了一个全局变量。使用范围广,让我们来拯救全球数据,实现全球业务,等等。。

java中单例模式是一种常见的设计模式,单例模式分三种:懒汉式单例、饿汉式单例、登记式单例三种。


  单例模式有一下特点:
  1、单例类仅仅能有一个实例。
  2、单例类必须自己自己创建自己的唯一实例。
  3、单例类必须给全部其它对象提供这一实例。
  单例模式确保某个类仅仅有一个实例,并且自行实例化并向整个系统提供这个实例。

在计算机系统中。线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。

每台计算机能够有若干个打印机,但仅仅能有一个Printer Spooler。以避免两个打印作业同一时候输出到打印机中。每台计算机能够有若干通信port,系统应当集中管理这些通信port。以避免一个通信port同一时候被两个请求同一时候调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

最初的实现-饿汉式单例

/**

 * 饿汉式单例类.在类初始化时。已经自行实例化

 * @author

 *

 */

public class Singleton {

    //私有的默认构造子

    private Singleton() {}

    //已经自行实例化

    private static final Singleton single = new Singleton();

    //静态工厂方法

    public static Singleton getInstance() {

        returnsingle;

    }

}

上面的代码尽管简单。可是有一个问题——不管这个类是否被使用。都会创建一个instance对象。假设这个创建过程非常耗时,比方须要连接10000次数据库(夸张了…:-))。而且这个类还并不一定会被使用,那么这个创建过程就是没用的。

怎么办呢?

 

懒汉式

将上面的代码改动例如以下:

/**

 * 饿汉式单例类.在类初始化时,已经自行实例化

 * @author

 *

 */

public class Singleton {

    //私有的默认构造子

    private Singleton() {}

    //已经自行实例化

    private static Singleton single = null;

    //静态工厂方法

    public static Singleton getInstance() {

    if(single == null){

        single = new Singleton();

    }

        return single;

    }

}

代码的变化有两处——首先,把single初始化为null,直到第一次使用的时候通过推断是否为null来创建对象。

由于创建过程不在声明处。所以那个final的修饰必须去掉。

 

我们来想象一下这个过程。

要使用Singleton,调用getInstance()方法。

第一次的时候发现single是null。然后就新建一个对象,返回出去。第二次再使用的时候。由于这个single是static的,所以已经不是null了,因此不会再创建对象,直接将其返回。

 

这个过程就成为lazy loaded,也就是迟载入——直到使用的时候才进行载入。

线程同步

上面的代码非常清楚,也非常easy。

然而就像那句名言:“80%的错误都是由20%代码优化引起的”。单线程下,这段代码没有什么问题,但是假设是多线程,麻烦就来了。我们来分析一下:

 

线程A希望使用Singleton,调用getInstance()方法。由于是第一次调用。A就发现single是null的,于是它開始创建实例,就在这个时候。CPU发生时间片切换。线程B開始运行。它要使用Singleton。调用getInstance()方法。相同检測到single是null——注意,这是在A检測完之后切换的。也就是说A并没有来得及创建对象——因此B開始创建。B创建完毕后,切换到A继续运行,由于它已经检測完了,所以A不会再检測一遍。它会直接创建对象。

这样,线程A和B各自拥有一个Singleton的对象——单例失败!

 

解决办法也非常easy,那就是加锁,代码优化例如以下:

/**

 * 饿汉式单例类.在类初始化时,已经自行实例化

 * @author

 *

 */

public class Singleton {

    //私有的默认构造子

    private Singleton() {}

    //已经自行实例化

    private static Singleton single = null;

    //静态工厂方法

    public synchronized static Singleton getInstance() {

    if(single == null){

        single = new Singleton();

    }

        return single;

    }

}

 

上面的代码又是非常清楚非常easy的,然而,简单的东西往往不够理想。这段代码毫无疑问存在性能的问题——synchronized修饰的同步块但是要比一般的代码段慢上几倍的!

假设存在非常多次getInstance()的调用。那性能问题就不得不考虑了!

 

让我们来分析一下。到底是整个方法都必须加锁,还是只当中某一句加锁就足够了?我们为什么要加锁呢?分析一下出现lazyloaded的那种情形的原因。原因就是检測null的操作和创建对象的操作分离了。假设这两个操作可以原子地进行。那么单例就已经保证了。于是,我们開始改动代码:

/**

 * 饿汉式单例类.在类初始化时,已经自行实例化

 * @author

 *

 */

public class Singleton {

    //私有的默认构造子

    private Singleton() {}

    //已经自行实例化

    private static Singleton single = null;

    //静态工厂方法

    public static Singleton getInstance() {

    synchronized(Singleton.class){

        if(single == null){

             single = new Singleton();

        }

    }

        return single;

    }

}

 

首先去掉getInstance()的同步操作。然后把同步锁载入if语句上。可是这种改动起不到不论什么作用:由于每次调用getInstance()的时候必定要同步。性能问题还是存在。假设……假设我们事先推断一下是不是为null再去同步呢?

/**

 * 饿汉式单例类.在类初始化时,已经自行实例化

 * @author

 *

 */

public class Singleton {

    //私有的默认构造子

    private Singleton() {}

    //已经自行实例化

    private static Singleton single = null;

    //静态工厂方法

    public static Singleton getInstance() {

   

    if(single == null){

        synchronized(Singleton.class){

              if(single == null){

                 single = new Singleton();

              }

        }

    }

        return single;

    }

}

 

还有问题吗?首先推断instance是不是为null,假设为null,加锁初始化;假设不为null,直接返回instance。

 

这就是double-checked locking设计实现单例模式。到此为止,一切都非常完美。我们用一种非常聪明的方式实现了单例模式。

 

从源头检查 

以下我们開始说编译原理。所谓编译。就是把源码“翻译”成目标代码——大多数是指机器代码——的过程。

针对Java。它的目标代码不是本地机器代码。而是虚拟机代码。编译原理里面有一个非常重要的内容是编译器优化。所谓编译器优化是指,在不改变原来语义的情况下,通过调整语句顺序,来让程序执行的更快。

这个过程成为reorder。

 

要知道。JVM仅仅是一个标准。并非实现。

JVM中并没有规定有关编译器优化的内容。也就是说,JVM实现能够自由的进行编译器优化。

 

以下来想一下。创建一个变量须要哪些步骤呢?一个是申请一块内存,调用构造方法进行初始化操作,还有一个是分配一个指针指向这块内存。这两个操作谁在前谁在后呢?JVM规范并没有规定。

那么就存在这么一种情况。JVM是先开辟出一块内存。然后把指针指向这块内存,最后调用构造方法进行初始化。

 

以下我们来考虑这么一种情况:线程A開始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先推断instance是否为null。依照我们上面所说的内存模型。A已经把instance指向了那块内存。仅仅是还没有调用构造方法。因此B检測到instance不为null。于是直接把instance返回了——问题出现了,虽然instance不为null。但它并没有构造完毕,就像一套房子已经给了你钥匙,但你并不能住进去,由于里面还没有收拾。此时,假设B在A将instance构造完毕之前就是用了这个实例,程序就会出现错误了!

 

于是,我们想到了以下的代码:

/**

 * 饿汉式单例类.在类初始化时,已经自行实例化

 * @author

 *

 */

public class Singleton {

    //私有的默认构造子

    private Singleton() {}

    //已经自行实例化

    private static Singleton single = null;

    //静态工厂方法

    public static Singleton getInstance() {

   

    if(single == null){

        Singleton s;

        synchronized(Singleton.class){

            s = single;

           

            if(s == null){

 

             synchronized(Singleton.class){

                  if(s == null){

                     s = new Singleton();

                  }

             }

             single = s;

            }

        }

    }

        return single;

    }

}

 

我们在第一个同步块里面创建一个暂时变量。然后使用这个暂时变量进行对象的创建。而且在最后把instance指针暂时变量的内存空间。写出这样的代码基于下面思想。即synchronized会起到一个代码屏蔽的作用,同步块里面的代码和外部的代码没有联系。

因此,在外部的同步块里面对暂时变量sc进行操作并不影响instance,所以外部类在instance=sc;之前检測instance的时候。结果instance依旧是null。

 

只是。这样的想法全然是错误的。同步块的释放保证在此之前——也就是同步块里面——的操作必须完毕,可是并不保证同步块之后的操作不能因编译器优化而调换到同步块结束之前进行。因此,编译器全然能够把instance=sc;这句移到内部同步块里面运行。

这样,程序又是错误的了!

解决方式

 

说了这么多,难道单例没有办法在Java中实现吗?事实上不然!

 

在JDK 5之后,Java使用了新的内存模型。volatilekeyword有了明白的语义——在JDK1.5之前。volatile是个keyword,可是并没有明白的规定其用途——被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因此,仅仅要我们简单的把instance加上volatilekeyword就能够了。

/**

 * 饿汉式单例类.在类初始化时,已经自行实例化

 *

 * @author

 *

 */

public class Singleton {

    // 私有的默认构造子

    private Singleton() {

    }

 

    // 已经自行实例化

    private volatile static Singleton single = null;

 

    // 静态工厂方法

    public static Singleton getInstance() {

 

       if (single == null) {

 

           synchronized (Singleton.class) {

              if (single == null) {

                  single = new Singleton();

              }

           }

       }

       return single;

    }

}

 

然而。这仅仅是JDK1.5之后的Java的解决方式,那之前版本号呢?事实上,还有另外的一种解决方式。并不会受到Java版本号的影响:

/**

 * 饿汉式单例类.在类初始化时,已经自行实例化

 *

 * @author

 *

 */

public class Singleton {

 

    private static class SingletonInstance {

       private static final Singleton instance = new Singleton();

    }

 

    // 私有的默认构造子

    private Singleton() {

    }

 

    // 静态工厂方法

    public static Singleton getInstance() {

 

       return SingletonInstance.instance;

    }

}

 

在这一版本号的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明白说明了的,因此不存在不论什么二义性。在这段代码中,由于SingletonClass没有static的属性,因此并不会被初始化。

直到调用getInstance()的时候,会首先载入SingletonClassInstance类,这个类有一个static的SingletonClass实例,因此须要调用SingletonClass的构造方法,然后getInstance()将把这个内部类的instance返回给使用者。由于这个instance是static的,因此并不会构造多次。

 

因为SingletonClassInstance是私有静态内部类,所以不会被其它类知道。相同。static语义也要求不会有多个实例存在。

而且,JSL规范定义,类的构造必须是原子性的。非并发的,因此不须要加同步块。相同。因为这个构造是并发的。所以getInstance()也并不须要加同步。

 

至此。我们完整的了解了单例模式在Java语言中的时候,提出了两种解决方式。

个人偏向于另外一种。而且Effiective Java也推荐的这样的方式。

登记式单例类

import java.util.HashMap;

import java.util.Map;

 

/**

 * 饿汉式单例类.在类初始化时,已经自行实例化

 *

 * 登记式单例类.

 * 类似Spring里面的方法。将类名注冊,下次从里面直接获取。

 * @author

 *

 */

public class Singleton {

 

    private static Map<String, Singleton> map = new HashMap<String, Singleton>();

    static {

       Singleton single = new Singleton();

       map.put(single.getClass().getName(), single);

    }

 

    // 私有的默认构造子

    private Singleton() {

    }

 

    //静态工厂方法,返还此类惟一的实例

    public static Singleton getInstance(String name) {

       if (name == null) {

           name = Singleton.class.getName();

           System.out.println("name == null" + "--->name=" + name);

       }

       if (map.get(name) == null) {

           try {

              map.put(name, (Singleton) Class.forName(name).newInstance());

           } catch (InstantiationException e) {

              e.printStackTrace();

           } catch (IllegalAccessException e) {

              e.printStackTrace();

           } catch (ClassNotFoundException e) {

              e.printStackTrace();

           }

       }

       return map.get(name);

    }

 

}

參考:

http://www.cnblogs.com/whgw/archive/2011/10/05/2199535.html

http://devbean.blog.51cto.com/448512/203501/

版权声明:本文博客原创文章,博客,未经同意,不得转载。

原文地址:https://www.cnblogs.com/gcczhongduan/p/4621735.html