singleton 单例模式

 

一、意图

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

我们怎么样才能保证一个类只有一个实例并且这个实例易于被访问呢?

如果将对象赋值给一个java静态变量,那么你必须在程序一开始就创建好对象。万一这个对象非常耗费资源,而程序在这次的执行过程中又一直没有使用到它,不就形成浪费吗?

一个更好的办法是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建(通过截取创建新对象的请求) ,并且它可以提供一个访问该实例的方法。

这就是S i n g l e t o n模式,我们可以在需要时才创建对象。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。

 

单例模式就是要确保类在内存中只有一个对象,该实例必须自动创建,并且对外提供访问接口。

优点  在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。

缺点  没有抽象层,因此扩展很难。 职责过重,在一定程序上违背了单一职责

 

二、单例模式的实现

第一种:懒汉,线程不安全

 1 public class LazyNotSecurtySingleton {
 2  
 3     //使用静态变量来记录Singleton类的唯一实例
 4     private static LazyNotSecurtySingleton instance;
 5  
 6     // 私有构造,确保只有自类内部才能访问
 7     private LazyNotSecurtySingleton() {
 8     }
 9  
10     //返回该类的实例, 有线程同步问题     
11     public static  LazyNotSecurtySingleton getInstance() {
12         if (instance == null) {
13             instance = new LazyNotSecurtySingleton();
14         }
15         return instance;
16     }
17 }

 

Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。)

但是以上实现没有考虑线程安全问题。所谓线程安全是指:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。显然以上实现并不满足线程安全的要求,在并发环境下很可能出现多个Singleton实例。

 

      1、当线程A进入到第12行时,检查instance是否为空,此时是空的。
  2、此时,线程B也进入到12行。切换到线程B执行。同样检查instance为空,于是往下执行13行,创建了一个实例。接着返回了。
  3、在切换回线程A,由于之前检查到instance为空。所以也会执行13行创建实例。返回。
  4、至此,已经有两个实例被创建了,这不是我们所希望的。 

下面我们提供线程安全的做法

 

第三种:饿汉式 

 1 public class EagerSingleton {  
 2     //私有的类成员常量  
 3     private static final EagerSingleton SINGLETON = new EagerSingleton();  
 4     //私有的默认构造方法,此类不能被继承  
 5     private EagerSingleton(){}  
 6     //静态工厂方法  
 7     public static EagerSingleton getInstance(){  
 8         return SINGLETON;  
 9     }  
10 } 

 

这种方式,我们依赖JVM在加载这个类时马上创建该类的唯一实例,避免了线程安全问题。不过,instance在类装载时就实例化没有达到lazy loading的效果.

java 中的Runtime类就是采用这种方式

 1 /*
 2  * Runtime:每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。
 3  * exec(String command)
 4  */
 5 public class RuntimeDemo {
 6     public static void main(String[] args) throws IOException {
 7         Runtime r = Runtime.getRuntime();
 8     
9 // r.exec("notepad"); 10 // r.exec("calc"); 11 // r.exec("shutdown -s -t 10000"); 12 r.exec("shutdown -a"); 13 } 14 } 15 16 /* 17 * class Runtime { 18 * private Runtime() {} 19 * private static Runtime currentRuntime = new Runtime(); 20 * public static Runtime getRuntime() { 21 * return currentRuntime; 22 * } 23 * } 24 */

 

第三种:懒汉 线程安全

 

 1 public class LazySecurtySingleton {
 2     
 3     private static LazySecurtySingleton instance;
 4     /**
 5      * 私有构造子,确保无法在类外实例化该类
 6      */
 7     private LazySecurtySingleton() {
 8     }
 9  
10     /**
11      * synchronized关键字解决多个线程的同步问题
12      */
13     public static synchronized LazySecurtySingleton getInstance() {
14         if (instance == null) {
15             instance = new LazySecurtySingleton();
16         }
17         return instance;
18     }
19  
20 }

 

静态工厂方法中synchronized关键字提供的同步是必须的,否则当多个线程同时访问该方法时,无法确保获得的总是同一个实例。然而我们也看到,在所有的代码路径中,虽然只有第一次引用的时候需要对instance变量进行实例化,但是synchronized同步机制要求所有的代码执行路径都必须先获取类锁。在并发访问比较低时,效果并不显著,但是当并发访问量上升时,这里有可能会成为并发访问的瓶颈。

但给方法加上synchronized后。所有getInstance()的调用都要同步了。其实我们只是在第一次调用的时候要同步。而同步需要消耗性能。这就是问题。

 

第四种:双重校验锁

可以使用“双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受很大的影响。那么什么是“双重检查加锁”机制呢?

所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,

进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

“双重检查加锁”机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。关于volatile参看07 volatile & java 内存模型

注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只只能用在java5及以上的版本。

 

 1 public class TwoLockSingleton {
 2     private volatile static TwoLockSingleton singleton;
 3     private TwoLockSingleton() {
 4     }
 5     public static TwoLockSingleton getInstance() {
 6         //先检查实例是否存在,如果不存在才进入下面的同步块
 7         if (singleton == null) {
 8             //同步块,线程安全的创建实例
 9             synchronized (TwoLockSingleton.class) {
10                 //再次检查实例是否存在,如果不存在才真正的创建实例
11                 if (singleton == null) {
12                     singleton = new TwoLockSingleton();
13                 }
14             }
15         }
16         return singleton;
17     }
18 }

这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。

 第五种:使用内部类  Initialization-on-demand holder    -- idiom

In software engineering, the Initialization on Demand Holder (design pattern) idiom is a lazy-loaded singleton.
In all versions of Java, the idiom enables a safe, highly concurrent lazy initialization with good performance.

 1 public class Something {
 2   private Something() {}
 3 
 4   private static class LazyHolder {
 5     private static final Something INSTANCE = new Something();
 6   }
 7 
 8   public static Something getInstance() {
 9     return LazyHolder.INSTANCE;
10    }
11 }

关于idiom  详见 http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom、

解释一下,因为java机制规定,内部类SingletonHolder只有在getInstance()方法第一次调用的时候才会被加载(实现了lazy),

而且其加载过程是线程安全的(实现线程安全)。内部类加载的时候实例化一次instance。

有兴趣可以阅读下面的文字:

 

The implementation of the idiom relies on the initialization phase of execution within the Java Virtual Machine (JVM) as specified by the Java Language Specification (JLS).[2] When the class Something is loaded by the JVM, the class goes through initialization. Since the class does not have any static variables to initialize, the initialization completes trivially. The static class definition LazyHolder within it is not initialized until the JVM determines thatLazyHolder must be executed. The static class LazyHolder is only executed when the static method getInstance is invoked on the class Something, and the first time this happens the JVM will load and initialize the LazyHolder class. The initialization of the LazyHolder class results in static variable INSTANCE being initialized by executing the (private) constructor for the outer class Something. Since the class initialization phase is guaranteed by the JLS to be serial, i.e., non-concurrent, no further synchronization is required in the static getInstance method during loading and initialization. And since the initialization phase writes the static variable INSTANCE in a serial operation, all subsequent concurrent invocations of the getInstance will return the same correctly initialized INSTANCE without incurring any additional synchronization overhead.

 

This gives a highly efficient thread-safe "singleton" cache, without synchronization overhead; benchmarking indicates it to be far faster than even uncontended synchronization.[3] However, the idiom is singleton-specific and not extensible to pluralities of objects (e.g. a map-based cache).

 

posted @ 2015-08-26 13:49  yweihainan  阅读(406)  评论(0编辑  收藏  举报