`

java单例模式探究(转载)

阅读更多
[size=medium][size=medium]转自:
http://blog.csdn.net/haydenwang8287/article/details/4188357
作为对象的创建模式[GOF95],单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。由定义可以总结出单例模式的要点有三个:一是单例类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。

在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

虽然从类图上看,单例模式是最简单的设计模式之一,但是真正正确地使用单例模式却不是那么简单的事。



首先看一个经典的单例实现。

public class Singleton {  
  
    private static Singleton uniqueInstance = null;  
  
   
  
    private Singleton() {  
  
       // Exists only to defeat instantiation.  
  
    }  
  
   
  
    public static Singleton getInstance() {  
  
       if (uniqueInstance == null) {  
  
           uniqueInstance = new Singleton();  
  
       }  
  
       return uniqueInstance;  
  
    }  
  
    // Other methods...  
  
}  

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

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

有很多种方法可以实现线程安全的单例模式,下面逐一介绍:

1. 一步到位的饿汉单例类
饿汉式单例类是在Java 语言里实现得最为简便的单例类。在类被加载时,就会将自己实例化。
public class Singleton {  
  
    private static Singleton uniqueInstance = new Singleton();  
  
   
  
    private Singleton() {  
  
       // Exists only to defeat instantiation.  
  
    }  
  
   
  
    public static Singleton getInstance() {  
  
       return uniqueInstance;  
  
    }  
  
    // other methods...  
  
}  

2. 改造经典模式
首先是最简单最直接的改造。

public class Singleton {  
  
    private static Singleton uniqueInstance = null;  
  
   
  
    private Singleton() {  
  
       // Exists only to defeat instantiation.      
  
    }  
  
   
  
    public synchronized static Singleton getInstance() {  
  
       if (uniqueInstance == null) {  
  
           uniqueInstance = new Singleton();  
  
       }  
  
       return uniqueInstance;  
  
    }  
  
    //Other methods...  
  
}  

通过synchronized关键字,同步了不同线程对getInstance()的访问。这就是所谓的懒汉模式。与饿汉式单例类不同的是,懒汉式单例类在第一次被引用时将自己实例化。这种简单实现的问题在于,每次访问getInstance()都需要同步操作,而事实上同步只在第一次访问时有意义。为了避免不必要的同步操作,在JDK1.5以后可以使用一种双重检查加锁的方法。

public class Singleton {  
  
    // volatile is very important for uniqueInstance consistency.  
  
    private volatile static Singleton uniqueInstance = null;  
  
   
  
    private Singleton() {  
  
       // Exists only to defeat instantiation.  
  
    }  
  
   
  
    public static Singleton getInstance() {  
  
       // first check no need to synchronize.  
  
       if (uniqueInstance == null) {  
  
           // second check need to synchronize, but only run limit times.  
  
           synchronized (Singleton.class) {  
  
              if (uniqueInstance == null) {  
  
                  uniqueInstance = new Singleton();  
  
              }  
  
           }  
  
       }  
  
       return uniqueInstance;  
  
    }  
  
    // Other methods...  
  
}  

volatile确保uniqueInstance被初始化为单例后的改变对所有线程可见,多线程能够正确处理uniqueInstance变量。getInstance()中包含两次判空操作,第一次判空每次访问都会执行,而第二次判空只在初始访问存在大量并发的情况下出现。通过两次判空避免了不必要的线程同步。之所以限制必须在JDK1.5后使用是因为,之前的Java存储模型不能保证volatile语义的完全正确实现。为了突破这种限制《Effective Java》中给出了一种精妙的解决方法,充分利用了Java虚拟机的特性。
public class Singleton {  
  
    // an inner class holder the uniqueInstance.  
  
    private static class SingletonHolder {  
  
       static final Singleton uniqueInstance = new Singleton();  
  
    }  
  
   
  
    private Singleton() {  
  
       // Exists only to defeat instantiation.  
  
    }  
  
   
  
    public static Singleton getInstance() {  
  
       return SingletonHolder.uniqueInstance;  
  
    }  
  
    // Other methods...  
  
}  

When the getInstance method is invoked for the first time, it reads SingletonHolder.uniqueInstance for the first time, causing the SingletonHolder class to get initialized.The beauty of this idiom is that the getInstance method is not synchronized and performs only a field access, so lazy initialization adds practically nothing to the cost of access. A modern VM will synchronize field access only to initialize the class.Once the class is initialized, the VM will patch the code so that subsequent access to the field does not involve any testing or synchronization.
3. 登记式单例类
登记式单例类是GoF 为了克服饿汉式单例类及懒汉式单例类均不可继承的缺点而设计的。
public class RegSingleton {  
  
    static private HashMap m_registry = new HashMap();  
  
    static {  
  
       RegSingleton x = new RegSingleton();  
  
       m_registry.put(x.getClass().getName(), x);  
  
    }  
  
   
  
    protected RegSingleton() {  
  
    }  
  
   
  
    public static RegSingleton getInstance(String name) {  
  
       if (name == null) {  
  
           name = "com.javapatterns.singleton.demos.RegSingleton";  
  
       }  
  
       if (m_registry.get(name) == null) {  
  
           try {  
  
              m_registry.put(name, Class.forName(name).newInstance());  
  
           } catch (ClassNotFoundException cnf) {  
  
              System.out.println("Couldn't find class " + name);  
  
           } catch (InstantiationException ie) {  
  
              System.out.println("Couldn't instantiate an object of type "+ name);  
  
           } catch (IllegalAccessException ia) {  
  
              System.out.println("Couldn't access class " + name);  
  
           }  
  
       }  
  
       return (RegSingleton) (m_registry.get(name));  
  
}  
  
}  
  
// sub-class implements RegSingleton.  
  
public class RegSingletonChild extends RegSingleton {  
  
    public RegSingletonChild() {  
  
    }  
  
   
  
    static public RegSingletonChild getInstance() {  
  
       return (RegSingletonChild) RegSingleton  
  
              .getInstance("com.javapatterns.singleton.demos.RegSingletonChild");  
  
    }  
  
   
  
    public String about() {  
  
       return "Hello, I am RegSingletonChild.";  
  
    }  
  
}  

在GoF 原始的例子中,并没有getInstance() 方法,这样得到子类必须调用的getInstance(String name)方法并传入子类的名字,因此很不方便。加入getInstance() 方法的好处是RegSingletonChild 可以通过这个方法,返还自已的实例。而这样做的缺点是,由于数据类型不同,无法在RegSingleton 提供这样一个方法。由于子类必须允许父类以构造子调用产生实例,因此,它的构造子必须是公开的。这样一来,就等于允许了以这样方式产生实例而不在父类的登记中。这是登记式单例类的一个缺点。GoF 曾指出,由于父类的实例必须存在才可能有子类的实例,这在有些情况下是一个浪费。这是登记式单例类的另一个缺点。

现在我们已经知道如何实现线程安全的单例类和如何使用一个注册表去在运行期指定单例类名,接着让我们考查一下如何安排类载入器、处理序列化以及单例模式与ThreadLocal的关系。
l Classloaders
在许多情况下,使用多个类载入器是很普遍的--包括servlet容器--所以不管你在实现你的单例类时是多么小心你都最终可以得到多个单例类的实例。如果你想要确保你的单例类只被同一个的类载入器装入,那你就必须自己指定这个类载入器;例如:
private static Class getClass(String classname)  
  
           throws ClassNotFoundException {  
  
       ClassLoader classLoader = Thread.currentThread()  
  
              .getContextClassLoader();  
  
   
  
       if (classLoader == null)  
  
           classLoader = Singleton.class.getClassLoader();  
  
   
  
       return (classLoader.loadClass(classname));  
  
}   

这个方法会尝试把当前的线程与那个类载入器相关联;如果classloader为null,这个方法会使用与装入单例类基类的那个类载入器。这个方法可以用Class.forName()代替。
l 序列化
如果你序列化一个单例类,然后两次重构它,那么你就会得到那个单例类的两个实例,除非你实现readResolve()方法,像下面这样:
view plain
public class Singleton implements java.io.Serializable {  
  
      
  
    public static Singleton INSTANCE = new Singleton();  
  
   
  
    protected Singleton() {  
  
       // Exists only to thwart instantiation.      
  
    }  
  
   
  
    private Object readResolve() {  
  
       return INSTANCE;  
  
    }  
  
}  

上面的单例类实现从readResolve()方法中返回一个唯一的实例;这样无论Singleton类何时被重构,它都只会返回那个相同的单例类实例。无论是singleton,或是其他实例受控(instance-controlled)的类,必须使用readResolve方法来保护“实例-控制的约束”。从本质上来讲,readResovle方法把一个readObject方法从一个事实上的公有构造函数变成一个事实上的公有静态工厂。对于那些禁止包外继承的类而言,readResolve方法作为保护性的readObject方法的一种替代,也是非常有用的。
l ThreadLocal
在利用Hibernate开发DAO模块时,我们和Session打的交道最多,所以如何合理的管理Session,避免Session的频繁创建和销毁,对于提高系统的性能来说是非常重要的,以下代码实现了Session管理功能。
import org.hibernate.HibernateException;  
  
import org.hibernate.Session;  
  
import org.hibernate.cfg.Configuration;  
  
   
  
public class HibernateSessionFactory {  
  
    private static String CONFIG_FILE_LOCATION = "/hibernate.cfg.xml";  
  
    private static final ThreadLocal threadLocal = new ThreadLocal();  
  
    private static Configuration configuration = new Configuration();  
  
    private static org.hibernate.SessionFactory sessionFactory;  
  
    private static String configFile = CONFIG_FILE_LOCATION;  
  
   
  
    static {  
  
       try {  
  
           configuration.configure(configFile);  
  
           sessionFactory = configuration.buildSessionFactory();  
  
       } catch (Exception e) {  
  
           System.err.println("%%%% Error Creating SessionFactory %%%%");  
  
           e.printStackTrace();  
  
       }  
  
    }  
  
   
  
    private HibernateSessionFactory() {  
  
    }  
  
   
  
    public static Session getSession() throws HibernateException {  
  
       Session session = (Session) threadLocal.get();  
  
   
  
       if (session == null || !session.isOpen()) {  
  
           if (sessionFactory == null) {  
  
              rebuildSessionFactory();  
  
           }  
  
           session = (sessionFactory != null) ? sessionFactory.openSession()  
  
                  : null;  
  
           threadLocal.set(session);  
  
       }  
  
   
  
       return session;  
  
    }  
  
// Other methods...  
  
} 


我们知道Session是由SessionFactory负责创建的,而SessionFactory的实现是线程安全的,采用前面提到的“饿汉模式”创建单例。多个并发的线程可以同时访问一个SessionFactory并从中获取Session实例,那么Session是否是线程安全的呢?很遗憾,答案是否定的。Session中包含了数据库操作相关的状态信息,那么说如果多个线程同时使用一个Session实例进行CRUD,就很有可能导致数据存取的混乱,你能够想像那些你根本不能预测执行顺序的线程对你的一条记录进行操作的情形吗?以上代码使用ThreadLocal模式的解决了这一问题。 只要借助上面的工具类获取Session实例,我们就可以实现线程范围内的Session共享,从而避免了线程中频繁的创建和销毁Session实例。当然,不要忘记在用完后关闭Session。
ThreadLocal和线程同步机制相比有什么优势呢?
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单、更方便,且结果程序拥有更高的并发性。ThreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了它们的身影,起着举足轻重的作用。
不过在使用线程池的情况下,使用ThreadLocal应该慎重,因为线程池中的线程是可重用的。
ThreadLocal的内容以后还要仔细学习一下[/size][/size]
分享到:
评论

相关推荐

    Java 单例模式 工具类

    Java中的单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供全局访问点。在Java编程中,单例模式常用于控制资源的访问,比如数据库连接池、线程池或者日志对象等。本篇文章将深入探讨如何在Java中...

    Java 单例模式 懒汉模式

    Java 单例模式 懒汉模式 //懒汉式 多线程中不可以保证是一个对象

    Java单例模式设计

    Java单例模式是一种常用的设计模式,它保证一个类只有一个实例,并提供全局访问点。这种模式在需要频繁创建和销毁对象的场景中,或者当对象昂贵时(如数据库连接),能够节省系统资源,提高效率。本篇文章将深入探讨...

    JAVA单例模式的几种实现方法

    ### JAVA单例模式的几种实现方法 #### 一、饿汉式单例类 饿汉式单例类是在类初始化时就已经完成了实例化的操作。这种实现方式简单且线程安全,因为实例化过程是在编译期间完成的,不会受到多线程的影响。 **代码...

    Java单例模式应用研究.pdf

    ### Java单例模式应用研究 #### 一、单例模式概述 单例模式(Singleton Pattern)作为一种最基本的创建型设计模式,其主要目的是控制一个类的实例化过程,确保在整个应用程序中仅存在一个实例,并且该实例能够被全局...

    Java实现多种单例模式

    在Java编程中,单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点。这种模式在需要频繁创建和销毁对象的场景中尤其有用,因为它可以节省系统资源并确保对象间的协调一致。以下是...

    Java单例模式实现静态内部类方法示例

    "Java单例模式实现静态内部类方法示例" Java单例模式是软件设计模式中最基本和最常见的一种设计模式,也是最容易理解的一种设计模式。它的主要思想是确保某个类只有一个实例,并且提供一个全局访问点来访问该实例。...

    JAVA单例模式的登录案例

    根据给定的信息,我们可以深入探讨Java单例模式的登录案例,并从中提炼出多个重要的知识点。 ### 单例模式概述 单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点。这种模式通常...

    java 单例模式(懒汉式与饿汉式)

    Java 单例模式(懒汉式与饿汉式) Java 单例模式是一种常用的软件设计模式,在它的可信结构中只包含一个被实例化单例的特殊类。通过单例设计模式可以把整系统中的一个类只有一个实例。单例设计模式又分为两种方式,...

    设计模式之单例模式(结合工厂模式)

    在Java等面向对象编程语言中,单例模式常用于管理共享资源,如数据库连接池、线程池或者配置文件等。结合工厂模式,可以进一步优化单例的创建过程,提高代码的可读性和可维护性。 单例模式的核心在于控制类的实例化...

    Java 单例模式线程安全问题

    Java 单例模式线程安全问题详解 Java 单例模式线程安全问题是指在 Java 中实现单例模式时,如何确保线程安全的问题。单例模式是指在整个应用程序生命周期中,只有一个实例存在的设计模式。这种模式可以提高性能,...

    java单例模式完全讲解.pdf

    非常详细的Java单例模式讲解的文档,请求通过,已经上传过一次了

    java单例模式课程.pdf

    Java单例模式是一种设计模式,它允许类只有一个实例,并提供一个全局访问点。在上述的场景中,我们探讨了如何通过单例模式来优化读取配置文件的过程。配置文件是许多应用程序的重要组成部分,它们通常存储应用程序...

    单例模式(饿汉模式、懒汉模式、DCL单例模式、枚举)

    单例模式是设计模式中的一种,它在软件工程中用于控制类的实例化过程,确保一个类只有一个实例,并提供一个全局访问点。这种模式在资源管理、缓存、日志记录等方面广泛应用。本文将详细讨论四种常见的单例实现方式:...

    详解JAVA单例模式及多种实现.doc

    以下是几种常见的Java单例模式实现方式: 1. **饿汉式(静态常量)**: 这种方式在类加载时即初始化实例,线程安全,但可能导致不必要的内存占用。 ```java public class Singleton1 { private Singleton1() {} ...

    17-Java单例模式的学习笔记1

    Java 单例模式是一种设计模式,它用于保证一个类只有一个实例,并提供全局访问点。这种模式在需要控制类的实例化次数,或者当类的创建是昂贵的操作时非常有用。以下是对不同单例实现方式的详细说明: 1. **懒汉式**...

    Java设计模式之单例模式讲解

    入名所示,该文件为最详细的Java单例模式讲解并附有讲解代码。主要讲了单例模式的几种方法,懒汉模式、饿汉模式、静态内部类模式。着重讲解了懒汉模式下怎么实现线程安全。饿汉模式和静态内部类模式如何设置能够避免...

    JAVA单例模式

    一、单例模式的介绍 二、单例模式的特点 三、单例模式的应用 四、单例模式使用的注意 五、单例模式的举例

Global site tag (gtag.js) - Google Analytics