概念:

在Java应用程序中,一个类Class只有一个实例存在

 

Singleton介绍:

1.介绍:也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

   2.实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

   3.注意事项:单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。

    4.实现方式:通常单例模式在Java语言中,有两种构建方式:
       饿汉方式:指全局的单例实例在类装载时构建。(一般认为这种方式要更加安全些)
       懒汉方式:指全局的单例实例在第一次被使用时构建。 

 

运用:

1)系统资源,如文件路径,数据库链接,系统常量等

2)全局状态化类,类似AutomicInteger的使用

优缺点:

1)节省内存有利于垃圾回收

2)只能使用在特定的环境下,受限制于JVM和容器

    单例作用范围的前提是在一个ClassLoad下。所以像分布式应用EJB就要用其它的方式来解决单例问题。

共同点:

l  有一个私有的无参构造函数,这可以防止其他类实例化它,而且单例类也不应该被继承(final),如果单例类允许继承那么每个子类都可以创建实例,这就违背了Singleton模式“唯一实例”的初衷。

l  单例类被定义为sealed,就像前面提到的该类不应该被继承,所以为了保险起见可以把该类定义成不允许派生,但没有要求一定要这样定义。

l  一个静态的变量用来保存单实例的引用。

l  一个公有的静态方法用来获取单实例的引用,如果实例为null即创建一个。

 

Demo:

分别列出多种实现方式,各分析其优缺点

1)静态成员直接初始化,或者在静态代码块初始化都可以

class Singleton{    


1.         private Singleton(){}    
2.         private static final Singleton singleton = new Singleton();    
3.         public static Singleton getInstance(){return singleton;}    
4.     }

  


 

     该实现只要在一个ClassLoad下就会提供一个对象的单例。但是美中不足的是,不管该资源是否被请求,它都会创建一个对象,占用jvm内存。从lazy initialization思想出发,出现了下2的写法

2)根据lazy initialization思想,使用到时才初始化。


1.     class Singleton{    
2.         private Singleton(){}    
3.         private static Singleton singleton ;    
4.         public static synchronized Singleton getInstance(){    
5.             if(singleton==null)    
6.                 singleton = new Singleton();    
7.             return singleton;           
8.     
9.     }


 

      该实现方法加了同步锁,可以有效防止多线程在执行getInstance方法得到2个对象。

缺点:只有在第一次调用的时候,才会出现生成2个对象,才必须要求同步。而一旦singleton 不为null,系统依旧花费同步锁开销,有点得不偿失。

因此再改进出现写法3

3)在2的基础上改进,改进标准:尽量减少锁资源(主要体现在执行时间,所占内存等)


1.     class Singleton{    
2.         private Singleton(){}    
3.         private static Singleton singleton ;    
4.         public static Singleton getInstance(){    
5.             if(singleton==null)//1  
6.                 synchronized(Singleton.class){//2  
7.                     singleton = new Singleton();//3  
8.     
9.             return singleton;           
10.   
11.   }


 

这种写法减少了锁开销,但是在如下情况,却创建了2个对象:

a:线程1执行到1挂起,线程1认为singleton为null

b:线程2执行到1挂起,线程2认为singleton为null

c:线程1被唤醒执行synchronized块代码,走完创建了一个对象

d:线程2被唤醒执行synchronized块代码,走完创建了另一个对象

所以看出这种写法,并不完美。

4)为了解决3存在的问题,引入双重检查锁定
public static Singleton getInstance(){    


1.             if(singleton==null)//1  
2.                 synchronized(Singleton.class){//2  
3.                     if(singleton==null)//3  
4.                         singleton = new Singleton();//4  
5.     
6.             return singleton;           
7.


 

     在同步锁代码块内部,再判断一次对象是否为null,为null才创建对象。这种写法已经接近完美:

a:线程1执行到1,已经进入synchronized的时候,线程挂起,线程1占有Singleton.class资源锁;

b:线程2执行到1,当它准备synchronized块时,因为Singleton.class被占用,线程2阻塞;

c:线程1被唤醒,判断出对象为null,执行完创建一个对象

d:线程2被唤醒,判断出对象不为null,不执行创建语句

     如此分析,发现似乎没问题。

     但是实际上并不能保证它在单处理器或多处理器上正确运行;

     问题就出现在singleton = newSingleton()这一行代码。它可以简单的分成如下三个步骤:

mem=newsingleton();//1
instance =mem;//2
ctorSingleton(instance);//3

      这行代码先在内存开辟空间,赋给singleton的引用,然后执行new 初始化数据,但是注意初始化是要消耗时间。如果此时线程3在执行步骤1的时候,发现singleton 为非null,就直接返回,那么线程3返回的其实是一个没构造完成的对象。

     我们期望1,2,3 按照反序执行,但是实际jvm内存模型,并没有明确的有序指定。

     这归咎于java的平台的内存模型允许“无序写入”。

5)在4的基础上引入volatile

代码如下:


1.     class Singleton{    
2.         private Singleton(){}    
3.         private static volatile Singleton singleton ;    
4.         public static Singleton getInstance(){    
5.             if(singleton==null)//1  
6.                 synchronized(Singleton.class){//2  
7.                     if(singleton==null)//3  
8.                         singleton = new Singleton();    
9.     
10.           return singleton;           
11.   
12.   }


 

      Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。

而volatile使用时有明确的规定:

1.          对变量的写操作不依赖于当前值;

2.          该变量没有包含在具有其他变量的不变式中;

—— 只有在状态真正独立于程序内其他内容时才能使用 volatile。

但是5的写法,虽然理论上似乎可以解决无序写入问题。实际上并非如此。

小结:

1)使用同步锁方法,内部锁存在不安全。

2)静态成员直接初始化。