单例模式(Singleton)看了就懂

单例,故名思议,一个只能建立一个实例的类。java

单例被普遍应用于Spring的bean(默认)、线程池、数据库链接池、缓存,还有其余一些无状态的类如servlet。程序员

一个不必多例的类实现了单例能够节约空间(显而易见),节省资源(线程、数据库链接)。数据库

单例模式有这么多好处,那咱们来实现它吧,首先想到的是建立一个对象要使用new方法,new方法调用的是类的构造函数,想要不被程序员随意的new对象能够将类的构造函数设为私有,而后再提供一个获取这个类实例的方法,因此就有了下面这个实现。缓存

一、只能正确运行在单线程模式下的单例实现:安全

 1 public class SingletonSingleThread {
 2 
 3     private static SingletonSingleThread instance;
 4     
 5     private SingletonSingleThread(){}
 6     
 7     public static SingletonSingleThread getInstance(){
 8         if(instance == null){
 9             instance = new SingletonSingleThread();
10         }
11         return instance;
12     }
13 }

以上代码只能保证在单线程下运行,在多线程环境下可能有多个线程在第8行时判断到instance当前是指向null,而后都去执行第9行的代码。多线程

若是读者了解过volatile修饰变量的做用,可能想到将以上代码修改为以下形式,由于volatile能够保证线程之间变量的“可见性”,就能够保证每一个线程在第8行判断的时候都是instance的最新引用了?函数

 1  public class SingletonSingleThread {
 2  
 3      private static volatile SingletonSingleThread instance;
 4      
 5      private SingletonSingleThread(){}
 6       
 7      public static SingletonSingleThread getInstance(){
 8          if(instance == null){
 9              instance = new SingletonSingleThread();
10         }
11         return instance;
12     }
13 }

可是第8行和第9行是两行代码,并非原子操做,彻底可能出现线程A执行经过第8行校验,准备执行第9行的时候,另外一个线程B来到第8行校验,也经过了校验。性能

实际上就算是代码中的一行指令也不是原子操做,在编译成.class文件后未必是一行字节码,就算是一行字节码,在解释执行或即时编译执行转化成机器码时也可能对应多条指令,以上结论原理不在本文介绍范围以内。spa

思考:java里有一个很方便的实现线程安全的synchronized修饰符,加上不就实现线程安全了吗?是的,下面这个实现就是在getInstance方法上简单加上synchronized修饰符。线程

二、对性能不敏感的多线程安全单例实现:

 1 public class SingletonSlow {
 2 
 3     private static SingletonSlow instance;
 4     
 5     private SingletonSlow(){}
 6     
 7     public synchronized static SingletonSlow getInstance(){
 8         if(instance == null){
 9             instance = new SingletonSlow();
10         }
11         return instance;
12     }
13 }

若是程序对一个单例实现的getInstance()方法效率不敏感可使用这种实现方式。好处就是直观,简单。坏处也显而易见,只有当SingletonSlow对象第一次被建立时是须要同步的,以后的调用synchronized都将是额外的负担。

 

思考:能不能只在第一次调用对象实例须要建立的时候才同步代码,其余时候不一样步代码的方法呢?有的。

三、DCL单例模式(双重锁检查) 注意:这种方式只能应用于JDK1.5及之后的版本,JDK1.5解决了volatile没法实现双重锁单例的bug

 1 public class SingletonDCL {
 2 
 3     private volatile static SingletonDCL instance;
 4 
 5     private SingletonDCL(){}
 6     
 7     public static SingletonDCL getInstance(){
 8         if(instance == null){
 9             synchronized(SingletonDCL.class){
10                 if(instance == null){
11                     instance = new SingletonDCL();
12                 }
13             }
14         }
15         return instance;
16     }
17 }

DCL方式虽然也用到了同步保证单例,可是它的效率要远远高于第2种实现方式。首先实例变量instance用volatile修饰,保证第8行拿到的是instance的最新引用,这行判断能够快速逃避instance已经被初始化的状况,固然这行代码仍是会出现多个线程都判断为真的状况,第9行的代码保证了10-12行代码只有一个线程能执行,第10行从新判断instance引用为空的意义在于解决如下状况的发生:

  ①若线程A和B同时经过第8行代码判断,等待进入同步块依次执行,若A先执行,则B得到执行时间时instance已经被线程A初始化了。

  ②若线程A经过第8行代码判断,进入同步块开始执行11行代码未执行完时,线程B执行到第8行判断经过,这时A同步代码块执行完毕,B虽然接下来不须要等待直接进入同步块,但instance也是已经被初始化过的。

思考:这个写法看上去很难理解,我想到一个实现方式,既然是静态变量类型,直接在变量声明时赋值,或者写一个静态块初始化不就行了,静态类型的变量初始化是伴随着类加载进行的,不也是线程安全的吗?确实。

四、利用类加载器实现的急切加载单例:

 1 public class SingletonEagerly {
 2 
 3     private static SingletonEagerly instance = new SingletonEagerly();
 4     
 5     private SingletonEagerly(){}
 6     
 7     public static SingletonEagerly getInstance(){
 8         return instance;
 9     }
10 }

为何叫急切加载单例呢,由于依赖于java的类加载机制,类被加载的时机未必是在须要使用类的对象时,也许在还不须要这个类的实例的时候类的实例就已经被初始化了,若是这个单例很耗费资源,咱们确定想采用懒加载的方式去实现他。

这种实现还有一个缺陷就是依赖于当前线程的ClassLoader(),若是类被多个ClassLoader加载(好比servlet),那就没法保证单例了。

思考:怎样避免类的实例只有在真正须要它的时候才被初始化呢? 能够用私有静态内部类实现。

五、利用类加载器实现的懒加载单例:

 1 public class SingletonLazyByClassLoader {
 2 
 3     private SingletonLazyByClassLoader(){}
 4     
 5     public static SingletonLazyByClassLoader getInstance(){
 6         return SingletonHolder.instance;
 7     }
 8     
 9     private static class SingletonHolder{
10         private static final SingletonLazyByClassLoader  instance= new SingletonLazyByClassLoader();
11     }
12 }

将类的惟一实例放在内部静态类里,这样外部只要不调用getInstance方法就不会有类加载器加载去加载内部静态类,而内部静态类是私有的,因此只有第一次instance方法被调用的时候才会初始化了类的实例变量。

遗憾的是这种实现方式仍然没法避免ClassLoader不同的问题。

 


若是实在对getInstance效率无要求,使用方式2实现单例最简单直观;

若是程序运行在Jdk1.5及以上的环境,又不会以为实现麻烦,强烈推荐使用方式3实现单例,高效、可靠;

若是嫌方式3麻烦或者运行在Jdk1.5之前的版本,则根据是否对懒加载有需求使用方式5或方式4,同时要保证程序中用到的类加载器是AppClassLoader或先代加载器是AppClassLoader(而且遵循双亲委托机制),不然单例会失效。

相关文章
相关标签/搜索