`

单例模式

 
阅读更多
摘自:博客园,小黎*chichy
Go for it ,向攻城师出发http://www.cnblogs.com/coffeeXiaoLi/p/3280250.html


Java单例模式
在GoF的23种设计模式中,单例模式是比较简单的一种。然而,有时候越是简单的东西越容易出现问题。下面就单例设计模式详细的探讨一下。
 
所谓单例模式,简单来说,就是在整个应用中保证只有一个类的实例存在。就像是Java Web中的application,也就是提供了一个全局变量,用处相当广泛,比如保存全局数据,实现全局性的操作等。
 
1. 最简单的实现
 
首先,能够想到的最简单的实现是,把类的构造函数写成private的,从而保证别的类不能实例化此类,然后在类中提供一个静态的实例并能够返回给使用者。这样,使用者就可以通过这个引用使用到这个类的实例了。
 
public class SingletonClass { 

  private static final SingletonClass instance = new SingletonClass(); 
    
  public static SingletonClass getInstance() { 
    return instance; 
  } 
    
  private SingletonClass() { 
     
  } 
    
}
 
如上例,外部使用者如果需要使用SingletonClass的实例,只能通过getInstance()方法,并且它的构造方法是private的,这样就保证了只能有一个对象存在。
 
2. 性能优化——lazy loaded
 
上面的代码虽然简单,但是有一个问题——无论这个类是否被使用,都会创建一个instance对象。如果这个创建过程很耗时,比如需要连接10000次数据库(夸张了…:-)),并且这个类还并不一定会被使用,那么这个创建过程就是无用的。怎么办呢?
 
为了解决这个问题,我们想到了新的解决方案:
 
public class SingletonClass { 

  private static SingletonClass instance = null; 
    
  public static SingletonClass getInstance() { 
    if(instance == null) { 
      instance = new SingletonClass(); 
    } 
    return instance; 
  } 
    
  private SingletonClass() { 
     
  } 
    
}
 
代码的变化有两处——首先,把instance初始化为null,直到第一次使用的时候通过判断是否为null来创建对象。因为创建过程不在声明处,所以那个final的修饰必须去掉。
 
我们来想象一下这个过程。要使用SingletonClass,调用getInstance()方法。第一次的时候发现instance是null,然后就新建一个对象,返回出去;第二次再使用的时候,因为这个instance是static的,所以已经不是null了,因此不会再创建对象,直接将其返回。
 
这个过程就成为lazy loaded,也就是迟加载——直到使用的时候才进行加载。
 
3. 同步
 
上面的代码很清楚,也很简单。然而就像那句名言:“80%的错误都是由20%代码优化引起的”。单线程下,这段代码没有什么问题,可是如果是多线程,麻烦就来了。我们来分析一下:
 
线程A希望使用SingletonClass,调用getInstance()方法。因为是第一次调用,A就发现instance是null的,于是它开始创建实例,就在这个时候,CPU发生时间片切换,线程B开始执行,它要使用SingletonClass,调用getInstance()方法,同样检测到instance是null——注意,这是在A检测完之后切换的,也就是说A并没有来得及创建对象——因此B开始创建。B创建完成后,切换到A继续执行,因为它已经检测完了,所以A不会再检测一遍,它会直接创建对象。这样,线程A和B各自拥有一个SingletonClass的对象——单例失败!
 
解决的方法也很简单,那就是加锁:
 
public class SingletonClass { 

  private static SingletonClass instance = null; 
    
  public synchronized static SingletonClass getInstance() { 
    if(instance == null) { 
      instance = new SingletonClass(); 
    } 
    return instance; 
  } 
    
  private SingletonClass() { 
     
  } 
    
}
 
是要getInstance()加上同步锁,一个线程必须等待另外一个线程创建完成后才能使用这个方法,这就保证了单例的唯一性。
 
4. 又是性能
 
上面的代码又是很清楚很简单的,然而,简单的东西往往不够理想。这段代码毫无疑问存在性能的问题——synchronized修饰的同步块可是要比一般的代码段慢上几倍的!如果存在很多次getInstance()的调用,那性能问题就不得不考虑了!
 
让我们来分析一下,究竟是整个方法都必须加锁,还是仅仅其中某一句加锁就足够了?我们为什么要加锁呢?分析一下出现lazy loaded的那种情形的原因。原因就是检测null的操作和创建对象的操作分离了。如果这两个操作能够原子地进行,那么单例就已经保证了。于是,我们开始修改代码:
 
public class SingletonClass { 

  private static SingletonClass instance = null; 
    
  public static SingletonClass getInstance() { 
    synchronized (SingletonClass.class) { 
      if(instance == null) { 
        instance = new SingletonClass(); 
      } 
    }     
    return instance; 
  } 
    
  private SingletonClass() { 
     
  } 
    
}
 
首先去掉getInstance()的同步操作,然后把同步锁加载if语句上。但是这样的修改起不到任何作用:因为每次调用getInstance()的时候必然要同步,性能问题还是存在。如果……如果我们事先判断一下是不是为null再去同步呢?
 
public class SingletonClass { 

  private static SingletonClass instance = null; 

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

  private SingletonClass() { 

  } 

}
 
还有问题吗?首先判断instance是不是为null,如果为null,加锁初始化;如果不为null,直接返回instance。
 
这就是double-checked locking设计实现单例模式。到此为止,一切都很完美。我们用一种很聪明的方式实现了单例模式。
 
5. 从源头检查
 
下面我们开始说编译原理。所谓编译,就是把源代码“翻译”成目标代码——大多数是指机器代码——的过程。针对Java,它的目标代码不是本地机器代码,而是虚拟机代码。编译原理里面有一个很重要的内容是编译器优化。所谓编译器优化是指,在不改变原来语义的情况下,通过调整语句顺序,来让程序运行的更快。这个过程成为reorder。
 
要知道,JVM只是一个标准,并不是实现。JVM中并没有规定有关编译器优化的内容,也就是说,JVM实现可以自由的进行编译器优化。
 
下面来想一下,创建一个变量需要哪些步骤呢?一个是申请一块内存,调用构造方法进行初始化操作,另一个是分配一个指针指向这块内存。这两个操作谁在前谁在后呢?JVM规范并没有规定。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。
 
下面我们来考虑这么一种情况:线程A开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照我们上面所说的内存模型,A已经把instance指向了那块内存,只是还没有调用构造方法,因此B检测到instance不为null,于是直接把instance返回了——问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果B在A将instance构造完成之前就是用了这个实例,程序就会出现错误了!
 
于是,我们想到了下面的代码:
 
public class SingletonClass { 

  private static SingletonClass instance = null; 

  public static SingletonClass getInstance() { 
    if (instance == null) { 
      SingletonClass sc; 
      synchronized (SingletonClass.class) { 
        sc = instance; 
        if (sc == null) { 
          synchronized (SingletonClass.class) { 
            if(sc == null) { 
              sc = new SingletonClass(); 
            } 
          } 
          instance = sc; 
        } 
      } 
    } 
    return instance; 
  } 

  private SingletonClass() { 

  } 
    
}
 
我们在第一个同步块里面创建一个临时变量,然后使用这个临时变量进行对象的创建,并且在最后把instance指针临时变量的内存空间。写出这种代码基于以下思想,即synchronized会起到一个代码屏蔽的作用,同步块里面的代码和外部的代码没有联系。因此,在外部的同步块里面对临时变量sc进行操作并不影响instance,所以外部类在instance=sc;之前检测instance的时候,结果instance依然是null。
 
不过,这种想法完全是错误的!同步块的释放保证在此之前——也就是同步块里面——的操作必须完成,但是并不保证同步块之后的操作不能因编译器优化而调换到同步块结束之前进行。因此,编译器完全可以把instance=sc;这句移到内部同步块里面执行。这样,程序又是错误的了!
 
6. 解决方案
 
说了这么多,难道单例没有办法在Java中实现吗?其实不然!
 
在JDK 5之后,Java使用了新的内存模型。volatile关键字有了明确的语义——在JDK1.5之前,volatile是个关键字,但是并没有明确的规定其用途——被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因此,只要我们简单的把instance加上volatile关键字就可以了。
 
public class SingletonClass { 

  private volatile static SingletonClass instance = null; 

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

  private SingletonClass() { 

  } 
    
}
 
然而,这只是JDK1.5之后的Java的解决方案,那之前版本呢?其实,还有另外的一种解决方案,并不会受到Java版本的影响:
 
public class SingletonClass { 
    
  private static class SingletonClassInstance { 
    private static final SingletonClass instance = new SingletonClass(); 
  } 

  public static SingletonClass getInstance() { 
    return SingletonClassInstance.instance; 
  } 

  private SingletonClass() { 

  } 
    
}
 
在这一版本的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明确说明了的,因此不存在任何二义性。在这段代码中,因为SingletonClass没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载SingletonClassInstance类,这个类有一个static的SingletonClass实例,因此需要调用SingletonClass的构造方法,然后getInstance()将把这个内部类的instance返回给使用者。由于这个instance是static的,因此并不会构造多次。
 
由于SingletonClassInstance是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。
 
至此,我们完整的了解了单例模式在Java语言中的时候,提出了两种解决方案。个人偏向于第二种,并且Effiective Java也推荐的这种方式。
分享到:
评论

相关推荐

    43丨单例模式(下):如何设计实现一个集群环境下的分布式单例模式?1

    单例模式是一种设计模式,旨在确保一个类只有一个实例,并提供全局访问点。在单例模式中,类的构造函数是私有的,防止外部直接创建对象,而是通过静态方法获取该类的唯一实例。单例模式的唯一性通常是在进程范围内,...

    单例模式介绍

    单例模式是软件设计模式中的一种,用于控制类的实例化过程,确保一个类在整个程序运行期间只有一个实例存在。在Android开发中,单例模式应用广泛,尤其在管理全局资源、提供公共服务或优化性能时非常实用。下面我们...

    使用单例模式创建学生管理系统(饿汉式、懒汉式)

    单例模式是软件设计模式中的一种,它的主要目的是确保一个类只有一个实例,并提供一个全局访问点。在Java或类似的面向对象编程语言中,单例模式常用于管理共享资源,如数据库连接池、线程池或者配置文件等。在这个...

    Java单例模式简单示例

    Java单例模式简单示例 Java单例模式是一种非常常用的设计模式,它可以确保某个类只有一个实例,并提供一个全局访问点。下面我们将通过一个简单的示例来介绍Java单例模式的定义和使用技巧。 单例模式的定义 单例...

    7种单例模式

    单例模式是软件设计模式中的一种经典模式,其主要目的是确保一个类只有一个实例,并提供一个全局访问点。这种模式在很多场景下非常有用,比如控制共享资源、管理配置对象等。下面将详细介绍七种常见的单例模式实现...

    Python单例模式实例详解

    Python中的单例模式是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这种模式常用于管理共享资源,如数据库连接、配置对象等,以避免资源的重复创建和销毁,提高效率。 单例模式的...

    使用Java单例模式实现一个简单的日志记录器.txt

    ### 使用Java单例模式实现一个简单的日志记录器 #### 一、单例模式简介 单例模式是一种常用的软件设计模式,在该模式中,一个类只能创建一个实例,并且提供了一个全局访问点来访问该实例。单例模式的主要优点包括...

    c# 单例模式的实现方法

    c# 单例模式的实现方法 单例模式是所有设计模式中最简单的一种,主要用于确保整个应用程序中只有一个实例存在。c# 中实现单例模式有多种方法,本文将详细介绍饿汉式和懒汉式两种实现方法。 饿汉式 饿汉式是最简单...

    PHP单例模式是什么 php实现单例模式的方法

    单例模式是设计模式的一种,它用于确保某一个类只有一个实例存在,而且自行实例化并向系统提供这个实例。在PHP中,单例模式的实现通常涉及以下几个关键点: 首先,需要有一个用于保存类的唯一实例的静态成员变量,...

    浅谈Spring单例Bean与单例模式的区别

    "浅谈Spring单例Bean与单例模式的区别" 本文主要介绍了Spring单例Bean与单例模式的区别,通过对比两者的定义、实现机制和应用场景,帮助读者更好地理解这两种概念的异同。 一、单例模式的定义和实现 单例模式是一...

    Java单例模式实例简述

    Java单例模式是一种常见的设计模式,它在软件开发中用于控制类的实例化过程,确保在任何情况下,对于特定类,系统中只有一个实例存在。这种模式对于那些需要频繁创建和销毁的对象,或者需要全局访问的对象,如配置...

    C#单例模式

    C#单例模式的应用,这里给的是C#里面一个单例模式的应用案例,面向对象过程中不可避免的需要用到单例模式,我在博客园中有详细说明单例模式怎么应用的,链接地址:...

    .Net 单例模式(Singleton)

    单例模式是一种常见的设计模式,用于确保一个类只有一个实例,同时向整个系统提供这个唯一的实例。这种模式在许多场景中都十分有用,例如,在一个系统中打印机假脱机程序(PrinterSpooler)应该是唯一的,以避免打印...

    php单例模式实现方法分析

    单例模式常用于控制实例的创建,以确保一个类只有一个实例,并且提供一个全局的访问点。在PHP开发中,单例模式非常有用,尤其是在需要限制对象实例数量时,比如数据库连接池的创建。 单例模式的核心思想在于它封装...

    C++ 单例模式的详解及实例

    C++ 单例模式的详解及实例 1.什么叫单例模式? 单例模式也称为单件模式、单子模式,可能是使用最广泛的设计模式。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。有很...

    Java 单例模式实现

    饿汉式 线程安全,调用效率高,但是不能延时加载。 public class Singleton { private static Singleton instance = new ...如果方法没有 synchronized,单例没有 volatile , 则线程不安全。 public class Singleton {

    java使用静态关键字实现单例模式

    Java 使用静态关键字实现单例模式 在软件开发中,单例模式是一种常用的设计模式,它可以确保某个类只有一个实例,并提供了全局访问点。Java 语言提供了多种方式来实现单例模式,其中使用静态关键字是其中的一种。...

    js单例模式的两种方案

    ### JS单例模式的两种实现方案 #### 一、引言 在JavaScript中,单例模式是一种常用的软件设计模式,其主要目的是确保一个类只有一个实例,并提供一个全局访问点。这种模式广泛应用于需要频繁创建的对象,例如日志...

    单例模式 分析代码优化方法

    单例模式分析代码优化方法 单例模式是23种设计模式之一,是比较简单的一种设计模式,它的目的是无论调用多少次,都返回同一个对象,它的特点是构造器私有化。单例模式分为两种结构,一种是懒汉式的,一种是饿汉式的...

    java单例模式及实现

    Java单例模式及实现 Java单例模式是一种常见的设计模式,确保某一个类只有一个实例,而且向这个系统提供这个实例。单例模式可以分为三种:懒汉式单例、饿汉式单例、登记式单例。 单例模式的要点 1. 某个类只能有...

Global site tag (gtag.js) - Google Analytics