设计模式--单例模式的探究

作为对象的创建模式[GOF95],单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。由定义可以总结出单例模式的要点有三个:一是单例类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。

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

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

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

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语义的完全正确实现。为了突破这种限制《EffectiveJava》中给出了一种精妙的解决方法,充分利用了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...
}

WhenthegetInstancemethodisinvokedforthefirsttime,itreadsSingletonHolder.uniqueInstanceforthefirsttime,causingtheSingletonHolderclasstogetinitialized.ThebeautyofthisidiomisthatthegetInstancemethodisnotsynchronizedandperformsonlyafieldaccess,solazyinitializationaddspracticallynothingtothecostofaccess.AmodernVMwillsynchronizefieldaccessonlytoinitializetheclass.Oncetheclassisinitialized,theVMwillpatchthecodesothatsubsequentaccesstothefielddoesnotinvolveanytestingorsynchronization.

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

现在我们已经知道如何实现线程安全的单例类和如何使用一个注册表去在运行期指定单例类名,接着让我们考查一下如何安排类载入器、处理序列化以及单例模式与ThreadLocal的关系。

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()代替。

序列化

如果你序列化一个单例类,然后两次重构它,那么你就会得到那个单例类的两个实例,除非你实现readResolve()方法,像下面这样:

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方法的一种替代,也是非常有用的。

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对象,需要强制类型转换。但JDK5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用。

概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单、更方便,且结果程序拥有更高的并发性。ThreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了它们的身影,起着举足轻重的作用。

不过在使用线程池的情况下,使用ThreadLocal应该慎重,因为线程池中的线程是可重用的。

原文地址:Java单例模式探究

相关推荐