`
feikiss
  • 浏览: 100049 次
  • 性别: Icon_minigender_1
  • 来自: 西安
社区版块
存档分类
最新评论

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

阅读更多
从360doc中截取下来的关于单例模式的分析,感觉不错,本着“不重复发明轮子的思想”我就采取了拿来主义,对格式稍加改造(符合iteye的发表格式。。。)粘贴至此。
作为对象的创建模式[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的关系。
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对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单、更方便,且结果程序拥有更高的并发性。ThreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了它们的身影,起着举足轻重的作用。
不过在使用线程池的情况下,使用ThreadLocal应该慎重,因为线程池中的线程是可重用的。

原文地址:Java单例模式探究
分享到:
评论

相关推荐

    【Java设计模式】你对单例模式了解多少,一文深入探究

    目录单例模式懒汉式单例模式未初始化问题解决Double Check 双重检查方案一:不让第二步和第三步重排序-DoubleCheck方案二:基于类初始化-静态内部类饿汉式饿汉式与懒汉式最大区别序列化破坏单例模式原理枚举单例基于...

    Head first java 设计模式源码 我将其整理成maven工程导入即可使用

    - 单例模式(Singleton):保证一个类只有一个实例,并提供全局访问点。在Java中,通常通过双重检查锁定(Double-Checked Locking)或者静态内部类来实现。 - 工厂模式(Factory):定义一个用于创建对象的接口,让...

    Java与设计模式

    **Java与设计模式** 在软件开发领域,设计模式是一种被广泛接受并应用于解决常见问题的通用解决方案。...通过阅读《Java与设计模式》的PPT,可以深入探究这些模式的细节和实际应用场景,从而提升自己的编程技能。

    c#设计模式随书源码

    例如,单例模式的实现通常包括一个私有的构造函数和一个静态方法,用于确保类只有一个实例;而工厂方法模式则通过抽象工厂类定义一个接口来创建对象,具体类型由子类决定,提高了代码的灵活性。 此外,策略模式允许...

    J2EE基础,设计模式

    Java企业版(J2EE,现称为Java EE)是一个用于构建可扩展、多层、分布式企业级应用的平台。...通过深入探究J2EE基础和设计模式的结合,开发者可以更好地应对复杂的业务需求,打造出高效、稳定的分布式系统。

    浅谈设计模式在Android代码重构中的应用.pdf

    文章通过具体的Android应用开发案例,展示了如何运用设计模式进行代码重构,例如,通过使用单例模式管理网络请求,使用工厂模式简化对象创建,使用观察者模式实现数据的实时更新等。 5. 提升信息素养的重要性 在...

    loki 库 源码 策略设计模式

    《洛基库(Loki)源码解析:策略设计模式的深度探究》 洛基库(Loki)是一个由C++编程语言实现的开源库,它以其强大的模板设计能力而闻名,尤其在策略设计模式的应用上表现突出。该库不仅为开发者提供了多种实用的...

    深入PHP:面向对象、模式与实践(第3版)(源码)

    - **单例模式**:学习如何确保一个类只有一个实例,并提供全局访问点。 - **工厂模式**:理解如何用工厂模式创建对象,以便于后期维护和扩展。 - **观察者模式**:研究如何实现对象间的依赖订阅,以及事件驱动...

    tom-spring源码文档,阿里java面经.rar

    - **单例模式**:Spring默认将bean定义为单例,保证应用中只有一个bean实例。 - **代理模式**:AOP(面向切面编程)中,Spring使用JDK动态代理或CGLIB实现方法拦截。 - **策略模式**:如不同类型的事务管理策略,...

    DesignPattern.rar

    设计模式是软件工程中的一种最佳实践,它是在特定上下文中解决常见问题的..."DesignPattern.rar"提供了一个全面学习设计模式的机会,无论你是初学者还是经验丰富的开发者,都应该深入探究,不断提升自己的编程素养。

    88个经典Android应用程序打包下载(源代码)

    - 分析源代码中的设计模式,如单例模式、工厂模式、观察者模式等。 - 学习如何编写可测试的代码,理解单元测试和集成测试的重要性。 - 了解如何使用MVVM、MVP等架构模式来组织代码。 8. **第三方库集成**: - ...

    《程序员面试经典》c++实现.zip

    这份资料涵盖了C++编程语言、面试常见问题、设计模式、数据结构、操作系统以及网络等多个关键领域的知识点。 1. **C++编程语言**: - 基本语法:包括变量声明、类型转换、运算符优先级等基础概念。 - 面向对象...

    深入分析Spring源码.docx

    Spring源码分析不仅仅是对具体实现的探究,更是对设计模式和软件工程原则的深入理解。通过对Spring源码的学习,开发者可以提升自己的设计能力,更好地理解和应用这些模式于实际项目中,提高代码质量和可维护性。因此...

    todo-with-pattern:待办事项模式研究

    这个项目通过实际的待办事项应用,向开发者展示了如何在JavaScript中有效地应用和组合设计模式,如工厂模式、单例模式、观察者模式、策略模式和装饰器模式等,以提高代码的可读性、可扩展性和可维护性。通过这个项目...

    Javaweb开发学习路线及Java三大框架分享?.docx

    - 研究 Java 设计模式,如外观模式、适配器模式、单例模式、观察者模式、装饰模式等。 - 了解 Web 服务架构,如 WebService、SCA、SOA、ESB、EAI,理解面向服务的架构。 - 探索面向资源架构(ROA)和 RESTful ...

    DesignPatterns:我在课程中进行的最后和正在进行的设计模式项目

    例如,单例模式确保一个类只有一个实例,并提供全局访问点,这在处理资源管理时非常有用。工厂方法模式允许子类决定实例化哪一个类,增强了代码的灵活性。建造者模式则将复杂对象的构建与它的表示分离,使得相同的...

    2019武汉小米笔试题

    - **单例模式**:学习如何实现线程安全的单例,了解其在系统设计中的作用。 - **工厂模式**:理解如何通过工厂模式来创建对象,提高代码的灵活性。 8. **网络编程** - **Socket通信**:掌握基于TCP/IP协议的...

    Java工作实用篇.pdf

    - 创建型模式: 如单例模式、工厂模式等。 - 结构型模式: 如适配器模式、装饰器模式等。 - 行为型模式: 如观察者模式、策略模式等。 - **应用价值**: - 解决常见问题的设计方案。 - 提高代码的复用性和可维护性...

    一个贯穿Java语言教学的任务实例探究.zip

    10. **设计模式**:学生会接触到一些常见的设计模式,如单例、工厂、观察者等,这些模式是解决特定问题的通用解决方案。 在实践中,每个项目都会围绕某个具体问题或需求展开,通过分析、设计、编码和测试,学生可以...

Global site tag (gtag.js) - Google Analytics