`
喻红叶
  • 浏览: 41092 次
  • 性别: Icon_minigender_1
  • 来自: 哈尔滨
社区版块
存档分类
最新评论

Java与模式-单例模式(一)

 
阅读更多

作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。与单例模式对应的,多例模式中的多例类可以有多个实例,多例类也必须自己创建、管理自己的实例,并向外界提供自己的实例。本文会着重探讨单例模式 ,并把多例模式也介绍一下。单例类看似简单,实则暗藏了很多坑,稍不注意就会出错。

单例模式的特点

单例模式的要点有三个:

(1)单例类只能有一个实例,将构造方法设为private,保证外部无法实例化该类,同时把它设为静态变量;

(2)它必须自行创建这个实例,在类的内部创建,且只创建一次;

(3)它必须自行向整个系统提供这个实例,提供一个静态的工厂方法,返回该类的实例。

从以上的描述中,我们已经可以初见单例模式的端倪。单例模式又分成饿汉式单例模式,懒汉式单例模式,登记式单例模式三种,下面我们一一道来。

1.饿汉式单例类

从单例模式的特点中,我们知道在单例类中定义一个静态域,类型就是这个类本身。饿汉式是一种很形象的叫法,就好像这个类很急迫,不管现在是否需要这个实例,在定义静态变量的定义处就把它初始化,提前初始化。

/**
 * 饿汉式单例模式
 * 在类加载的初始化阶段,instance会被初始化,单从资源利用角度来看,稍差
 * 线程安全的
 * @author cxy
 */
public class EagerSingleton {
	private static final EagerSingleton instance = new EagerSingleton();
	//私有构造方法,保证外界无法直接实例化
	private EagerSingleton() {
		//初始化操作
	}
	//静态工厂方法,线程安全
	public static EagerSingleton getInstance() {
		return instance;
	}
}

饿汉式单例类是线程安全的,但是它在一定程度上造成了浪费,尤其是某些框架里的工厂类,它可能会持有很多类的实例,如果使用饿汉式,那么 会造成加载代价太大。由此,又引出了懒汉式单例类。

2.懒汉式单例类

懒汉式单例类就是采用延迟初始化,延迟到需要域的值时才将它初始化,如果永远不需要这个值,那么这个域就永远不需要初始化。

/**
 * 懒汉式单例模式,使用延迟初始化
 * instance直到使用时才初始化
 * 在多线程环境下,必须同步getInstance()方法
 */
public class LazySingleton {
	private static LazySingleton instance = null;
	//私有的构造方法,保证外界无法直接实例化
	private LazySingleton() {
		System.out.println("只能有一个实例");
	}
	
	/*必须得是synchronized,否则在多线程下会出现多个实例*/
	public synchronized static LazySingleton getInstance() {
		if(instance == null) {
			
			//在非同步的情况下,为了看到多于一个的线程在创建实例
			Thread.yield();
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			instance  = new LazySingleton();
		}
		return instance;
	}
}

一定要注意,getInstance()是一个同步方法,这是为了保证在多线程下,该类仍然只有一个实例。如果不是同步方法会发生什么呢?考虑下面的情况:

(1)线程1调用getInstance(),它首先检查instance是否为空,也就是执行这句话:if(instance == null),一看确实是空的,准备创建吧,恰在这时,它让出了CPU;

(2)线程2也调用getInstance(),它也检查instance是否为空,结果为空,创建实例,现在该类有了第一实例了,完成,返回;

(3)线程1又开始运行了,它该执行这句话了:instance = new LazySingleton();又创建了该类的一个实例,这样它就有两个实例了,这已经不符合要求了。

所以,如果getInstance()必须得同步。但是,创建仅仅只发生了一次,更多的行为是返回这个实例,把getInstance()变成同步方法后,这样每次只能有一个线程调用该方法。为了一个单一行为,却牺牲了常态行为的性能,这样好吗?(注意,如果这个类是不可变的,那么即使getInstance()不同步,它也是安全的)。

双重成例检查

我们分析一下getInstance()方法:其实我们真正要同步的仅仅只是这一句话:instance = new LazySingleton()。明确了这一点后,看下面的代码

if(instance == null) {//第一次检查
	synchronized(LazySingleton.class) {
		if(instance == null) {//第二次检查
			instance = new LazySingleton();
		}
	}
}
这一段理解起来也没有那么难:

(1)说先判断instance是否为空,若为空则去创建该实例;

(2)为了保证只有一个实例,创建语句需要同步,只有获得LazySingleton对应的Class对象的锁之后,才能去执行创建代码;

(3)终于获得了Class对象的锁,要去创建实例了。等一等,刚才可能已经有其他的线程创建了该实例,还是需要判断一下,实例是否为空,若为空,则说明我是第一个进入到同步块的线程,实例还没有创建,那么就让我一个人在毫无打扰的环境下工作吧;若不为空,则说明已经有其他线程创建过实例了,我还是乖乖的享用别人的成果吧。

这样只有第一次创建实例的时候才需要同步,获取实例的时候,由于实例不为空,根本不用进入同步块,这效率,唰唰唰。多么天才的一个想法!接下来,我知道了,这个想法并不是我创造的,之前的人早就使用了,这就是在C语言中大名鼎鼎的双重成例检查(DCL,Double-Checked Locking),如名所示,进行了两次是否为空的检查。可是,双重成例检查在Java中不可行,它已经臭名昭著了,只是他们的解释,着实让人看不懂!

在《Java concurrency in practice》的16.2中,和《Effective Java 2th Edition》第七十一条中,作者提到了真正的原因:在Java 1.5之前的版本中,由于volatile修饰符的语义不够强,双重检查模式的功能很不稳定。他们是JMM规范的制定者,我觉得他们的话确凿无疑的可信。在JMM的后续版本(1.5及以后)中,将instance声明为volatile,那么就能安全的启用DCL了。之所以会出现DCL,是因为JVM在无竞争同步的执行速度很慢,经过优化后,这些都不是问题了,所以使用双重成例检查并不是一种高效的优化措施。

《Effective Java》的第71条:慎用延迟加载。在大多数情况下,正常的初始化要优于延迟初始化。经过衡量之后,必须采用延迟初始化,分两种情况:首先是实例域,采用volatile配合DCL(1.5及以后版本)。

/**
 * 在Java 5.0之后的版本中,使用volatile和DCL可以解决
 * 多线程环境下,实例域的延迟初始化
 */
public class VolatileAndDCL {
	private volatile  VolatileAndDCL instance;
	//私有构造方法
	private VolatileAndDCL() {
		//执行初始化
	}
	/*
	 * 使用双重成例检查,必须配合volatile使用
	 * 只有第一次创建实例的时候需要进入同步块
	 */
	public  VolatileAndDCL getInstance() {
		//使用局部变量result可以提高性能(参考数据:25%),而且更加优雅
		VolatileAndDCL result = instance;
		//第一次检查,只有为空时,才进入同步块
		if(result == null) {
			synchronized(VolatileAndDCL.class) {
				//第二次检查,如果仍为空,创建
				if(result == null)
					instance = result = new VolatileAndDCL();
			}
		}
		return result;
	}
}

对局部变量result的使用让人感到困惑,作者解释到使用局部变量result可以提高性能,而且更加优雅。注意,这里的field是实例域,当然了静态域也可以使用DCL,但是我们有更好的选择:lazy initialization holder class(也称作initialize-on-demand holder class idiom,延迟初始化站位类模式),这种模式使用一个静态内部类持有外围类静态域(也就是instance):

class StaticFieldSingleton {
	/**
	 * 使用静态内部类来延迟初始化instance,
	 * 在加载StaticFieldSingleton时,不会对FieldHolder初始化
	 * 直到读取field时才对FieldHolder初始化,也就是对instance初始化
	 */
	private static class FieldHolder {
		static final StaticFieldSingleton instance = new StaticFieldSingleton();
	}
	
	private StaticFieldSingleton() {
		//执行一些初始化操作
	}
	//完全不需要同步
	public static StaticFieldSingleton getInstance() {
		return FieldHolder.instance;
	}
}

当加载StaticFieldSingleton类时,并不会对初始化FieldHolder类,只有第一次读取getInstance()时,FieldHolder类才会初始化。这种模式的魅力在于:首先它保证了延迟初始化的优点,同时getInstance()又没有被同步,也就没有增加任何访问开销。现代的VM在初始化FieldHolder类时,会同步对intance的访问,保证多线程下的初始化过程中的安全性。一旦这个类被初始化,VM将修补代码,以便后续对instance的访问不会导致任何测试或同步。这个思路,简直就简直了!

通过对懒汉式单例模式的分析,我们大致可以得出如下结论:慎用延迟初始化,必须经过测量类在用和不用延迟初始化时的性能差别之后,再做决定。如果是实例域的延迟初始化,那么就是用volatile配合DCL;如果是静态域的延迟初始化,使用lazy initialization holder class模式。

3.登记式单例模式

在单例模式中,要求构造方法是private,这样外部无法直接实例化该类的对象,但是构造方法是private,也就才造成了该类不可被继承的缺点。为了克服这个缺点,GoField又提出了登记式单例模式,在父类中使用一个map来持有所有子类的实例,也就是说:说有子类到父类的map中登记,当要获得该类实例时,去Map中获得该类的实例。

/**
 * 登记式单例类
 * 是为了克服饿汉式和懒汉式均不能继承的缺点而设计的
 * 子类的实例化只能是懒汉式
 * @author cxy
 */
public class RegSingleton {
	//所有子类都要到registry中去登记
	private static HashMap<String,RegSingleton> registry = new HashMap<String, RegSingleton>();
	//饿汉式实例化 
	static {
		RegSingleton instance = new RegSingleton();
		registry.put(instance.getClass().getName(), instance);
	}
	
	//为了让子类可以继承,不能为私有
	protected RegSingleton() {	}
	
	/**
	 * 静态工厂方法,返回指定类的唯一实例。
	 * 首先去registry中查找是否有该类的实例,如有,直接返回。
	 * 如果没有,使用反射机制生成该类的实例,并放到registry中。
	 */
	public static RegSingleton getInstance(String name) {
		//如果为空,则返回此类的实例
		if(name == null) {
			name = "com.javapatterns.singleton.RegSingleton";
		}
		/**
		 * 如果返回的是null,说明这个子类没有登记过
		 * 则创建子类的实例,并且登记到registry中
		 */
		if(registry.get(name) == null) {
			try {
				//在Java中,由于反射的存在,可以让子类的构造方法是私有的,就像下面注释的那样实现
				//但这已经是语言的特性了,超出了设计模式的范畴了
				//RegSingleton t_instance =  (RegSingleton) Class.forName(name).getDeclaredConstructors()[0].newInstance();
				registry.put(name, (RegSingleton) Class.forName(name).newInstance());
			}catch(Exception e) {
				System.out.println("实例化对象失败");
			}
		}
		return registry.get(name);
		
	}
}

/**
 * 子类需要父类的帮助才能实例化
 */
 class RegSingletonChild extends RegSingleton {
	public RegSingletonChild(){}
	
	/**
	 * 静态工厂方法
	 */
	public static RegSingletonChild getInstance() {
		return (RegSingletonChild)RegSingleton.getInstance("com.javapattens.singleton.RegSingletonChild");
	}
}

通过上面的代码,我们可以看出父类的构造方法不再是private的了,这是登记式单例模式的一个缺点。这个登记式单例模式是根据Java语言的特点来写的,还有其他的形式。

使用单例模式的条件

使用单例模式有一个必要条件:在一个系统要求一个类只有一个实例时才应当应用单例模式。反过来说,如果一个类可以有几个实例共存,那么就没有必要使用单例类。

模式学习总是简单,但是如何使用,在什么情况下,却是一个很需经验和技巧的事情。且学且珍惜吧!

上面介绍了单例模式的特点,三种类型的单例模式,以及各自的优缺点及注意事项,着重谈论了懒汉式单例模式的延迟初始化问题,这一部分已经超出了设计模式的范畴了,但是,我觉得仍然很有必须花时间去弄明白。一般情况下,饿汉式单例模式已经够使用了,如果需要延迟加载,本文也已经给出了完美的解决方案。由于单例模式内容很多,因此分成两篇来探讨,下一篇讲一下多例模式,以及单例模式和多例模式的一个实际应用。

----------------------------------------------------------------- 更新--------------------------------------------------------------------------------------

单元素的枚举

Java 1.5中提供了枚举类型,枚举类型是实现单例类的最佳方法,而且更加简洁。由于枚举类实现了Serializable接口,使用枚举实现的单例类可以直接享受枚举提供了的序列化机制,绝对多次实例化。使用枚举出了可以实现单例,还可以实现有限例,其情形与单例相似,给出示意性代码:

public enum Singleton {
	Instance(some args);
	
	private Singleton(some args) {
		//初始化
	}
	
	//提供的方法
	public void f() { }
	public XXX xxx(xxx) { }
}

参考资料:《Java与模式》,《Effective Java 2th Edition》,《Java并发编程实战》,GoF《设计模式》。


分享到:
评论

相关推荐

    java设计模式----单例模式

    单例模式是其中最常用的一种,它的核心思想是确保一个类只有一个实例,并提供全局访问点。单例模式的应用场景包括:控制资源的访问、管理复杂的初始化过程以及在系统中创建一个全局的配置对象等。 单例模式分为两种...

    Java设计模式-单例模式详解

    Java设计模式-单例模式详解 单例模式是 Java 设计模式中的一种常用的设计模式,旨在保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式的目的是为了保证在一个进程中,某个类有且仅有一个实例。 ...

    JAVA-设计模式-创建型模式-单例模式

    JAVA-设计模式-创建型模式-单例模式

    java设计模式-单例.pdf

    ### Java设计模式——单例模式详解 #### 一、单例模式概述 单例模式是设计模式中的一个重要组成部分,属于创建型模式之一。其主要作用是确保某个类仅有一个实例存在,并提供一个全局访问该实例的方法。这在很多场景...

    java设计模式-单例模式

    设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中...这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

    设计模式-单例模式

    单例模式是一种广泛应用于软件设计中的创建型设计模式,它的核心思想是确保一个类只有一个实例,并提供一个全局访问点。这样做的好处在于控制共享资源的访问,比如线程安全的数据库连接池或者配置管理等。 在Java中...

    设计模式--单例模式java例子

    单例模式是软件设计模式中的一种经典模式,它在Java编程中被广泛使用。这个模式的主要目的是确保一个类只有一个实例,并提供一个全局访问点。这样做的好处在于可以控制实例的数量,减少资源消耗,同时便于协调整个...

    ava常用设计模式-单例模式

    ava常用设计模式-单例模式 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,有以下特点: 1. 单例类只能有一个实例。 2. 单例类必须自己创建自己的唯一实例。 3. 单例类必须给所有其他对象提供这一...

    常见设计模式-单例模式

    设计模式-单例模式 单例模式是一种常见的设计模式,它的主要作用是确保在内存中仅创建一次对象,并提供一个全局访问点。单例模式有两种类型:饿汉类型和懒汉类型。 饿汉类型的单例模式是在类加载时创建对象,例如...

    Java-设计模式-单例模式-实现源码(简单实现、双重检查锁、静态内部类、枚举类)

    单例模式是软件设计模式中的一种经典模式,其主要目的是保证一个类只有一个实例,并提供一个全局访问点。在Java中,有多种实现单例模式的方法,包括简单实现、双重检查锁定(Double-Checked Locking)、静态内部类和...

    java-单例模式几种写法

    单例模式是软件设计模式中的一种,用于控制类的实例化过程,确保一个类只有一个实例,并提供全局访问点。在Java中,实现单例模式有多种方法,每种方法都有其特点和适用场景。以下是对这六种常见单例模式实现方式的...

    设计模式-单例设计模式

    单例模式是一种常见的创建型设计模式,其核心思想在于确保一个类仅有一个实例存在,并且该实例由该类自行创建,随后向整个系统提供这一唯一实例。 #### 类图解析 在设计模式中,类图是一种直观展示类结构及关系的...

    设计模式-单例模式(讲解及其实现代码)

    单例模式是软件设计模式中的一种,它保证一个类只有一个实例,并提供全局访问点。这种模式在许多场景下非常有用,比如控制共享资源、管理系统级别的对象,如数据库连接池或者线程池等。单例模式的核心在于限制类的...

    1.设计模式-单例设计模式1

    在Java中,实现单例模式通常有多种方式,包括懒汉模式、饿汉模式、静态内部类以及枚举类型。 1. **懒汉模式**: 懒汉模式在第一次需要实例时才进行实例化,因此也被称为延迟加载。但是,这种模式存在线程安全问题...

    Java SE程序 单例模式

    Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式...

    java与模式-阎宏

    2. 创建型模式:包括单例模式、工厂方法模式、抽象工厂模式、建造者模式和原型模式。这些模式主要关注对象的创建过程,使得代码更加灵活,易于管理和测试。 3. 结构型模式:包括适配器模式、桥接模式、组合模式、...

    java单例模式实例

    单例模式是软件设计模式中的一种经典模式,用于确保一个类只有一个实例,并提供一个全局访问点。在Java中,有多种实现单例模式的方法,每种都有其特点和适用场景。接下来,我们将深入探讨这些实现方式。 首先,我们...

    设计模式单例模式和工厂模式综合应用

    "设计模式单例模式和工厂模式综合应用"的主题聚焦于两种常用的设计模式:单例模式和工厂模式,并探讨它们如何协同工作来实现高效、灵活的代码结构。这个主题尤其适用于Java编程语言,因为Java的面向对象特性使得设计...

    Java与模式---闫宏

    《Java与模式---闫宏》这...《Java与模式---闫宏》这本书通过丰富的例子和实际应用场景,帮助读者掌握设计模式的精髓,提升软件设计能力,对于任何希望提升自己编程水平的Java开发者来说,都是一本不可多得的经典读物。

Global site tag (gtag.js) - Google Analytics