单例模式

前言

单例模式(Singleton),又叫单件模式,是23个常用设计模式中最简单的一种,简单到不能称之为设计模式,而称为一种设计经验,也是IT面试时最常被问到的设计模式。可话又说回来了,设计模式,哪一种不是设计经验的总结呢?个人感觉,设计模式本质上都是对面向对象编程思想的总结升华。以Java为例,使用面向过程的思想,仍然能够完成工程代码,大部分Java初学者都在这样做;使用设计模式,可以设计出更好地类设计,使代码更具可读性、扩展性和可维护性。而且,学习设计模式的过程可以帮助理解面向对象的编程思想,单单以现实世界(例如动物,鸡鸭猫狗等现实世界的事物类比Java里面的父类、继承等概念)是无法理解面向对象思想的,特别是Java中“针对接口编程”等。从学习面向对象编程的角度看,学习设计模式越早越好,而非在Java代码量积累到一定程度之后再学。

定义

单例(单件)模式,确保1个类只有1个实例,并提供1个全局访问点。——《Head First 设计模式》

单例模式的目的是将某个类的实例限制为1个,这里的“1个”是针对1个ClassLoader而言的,如果使用多个ClassLoader,可能导致单例失效而产生多个实例。

在只保留1个实例的情况下,内存开销将大大降低;同步也将变得简单。最典型的单例模式的应用是任务管理器,1次只能打开1个任务管理器窗口。其他常用的场景包括数据库连接、网站应用配置对象,详情可参考《设计模式之——单例模式(Singleton)的常见应用场景

实现方式

方式一:单线程环境下

由于只能存在1个实例,所以构造函数不能设置为公开的、由客户端去随意的new(),只能设置为私有的(这样的类是无法被继承的)。在这种情况下,需要通过静态方法给客户端的调用。代码如下:


 * @Description: TODO
 * @author     : cmm
 * @date       : 2014-8-25 下午12:36:11
 *
 */

public class WebsiteConfig {
	
	private static WebsiteConfig uniq = null;
	
	private WebsiteConfig() {}
	
	public static WebsiteConfig getInstance() {
		if (uniq == null) {
			
//			try {
//				Thread.sleep(2 * 100);
//			} catch (InterruptedException e) {
//				e.printStackTrace();
//			}
			
			uniq = new WebsiteConfig();
			System.out.println("constructor run...");
		} else {
			System.out.println("instance existed...");
		}
		
		return uniq;
	}
	

	/**
	 * @Title:       main
	 * @Description: TODO
	 * @param        @param args
	 * @return       void
	 * @throws
	 */

	public static void main(String[] args) {
		
		int count = 100;
		
		Thread threads[] = new Thread[count];
		
		for (int i = 0; i < count; i++) {
			threads[i] = new Thread(new Runnable() {

				@Override
				public void run() {
					WebsiteConfig.getInstance();
				}
			});
			
			threads[i].start();
		}
	}
}

如果不考虑多线程的话,即如果该实例每次只被一个线程访问,上面的代码已经能满足只产生一个实例的要求了。但是如果存在多个线程同时调用WebsiteConfig.getInstance(),就会出现问题。main()中即为测试代码,同时开100个线程创建实例,为了是效果更明显,我们可以放开getInstance()中备注是掉的代码。某次运行结果如下:

constructor run...
constructor run...
constructor run...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
instance existed...
可以看出,new WebsiteConfig()被执行了3次,即有3个实例被创建,显然不符合单例模式的定义。

究其原因,可用一张图解释:

单例模式_第1张图片

如图,2个线程在第3和4步先后进入创建实例的代码行,结果多个实例被创建成功。

方式二:满足多线程环境

如存在多个线程操作访问的情况,则需要额外的代码进行同步,确保只有1个实例被创建。在Java中,我们可以使用synchronized关键字来进行同步getInstance(),使得每次只有1个线程进入该方法,就不会出现多个实例被创建的情况。代码如下:

	public synchronized static WebsiteConfig getInstance() {
		if (uniq == null) {
			
			try {
				Thread.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			uniq = new WebsiteConfig();
			System.out.println("constructor run...");
		} else {
			System.out.println("instance existed...");
		}
		
		return uniq;
	}
其实,只有第1个线程进入时候才需要同步,之后其他线程进入该方法都无需同步,直接进入该方法即可,因为第1个线程已经创建实例,其他线程再进入该方法时,会直接返回该实例。多余的同步会大大降低程序的性能,同步1个方法可能造成程序执行效率降低100倍。如果getInstance()将被频繁调用,我们就要换个方式来实现单例了。

方式三:饿汉方式

急切(eagerly)创建单例,代码如下:

	private static WebsiteConfig uniq = new WebsiteConfig();;
	
	private WebsiteConfig() {}
	
	public static WebsiteConfig getInstance() {
		return uniq;
	}
在jvm加载WebsiteConfig.class的时候,就马上创建唯一的单例实例。在任何线程访问静态变量uniq之前,该实例已经存在。其中,JDK中的Runtime类就是单例的典型应用,而且就是使用这种创建方式,Runtime.java源代码如下:

    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

方式四:双重检查锁(Double-Checked Locking)

另外1种方式是使用传说中的“双重检查锁(Double-Checked Locking)”,首先检查实例是否创建,未创建才进行同步,这样就只会同步1次,大大降低同步带来的性能开销。代码如下:

	private static volatile WebsiteConfig uniq = null;
	
	private WebsiteConfig() {}
	
	public static WebsiteConfig getInstance() {
		
		if (uniq == null) {
			synchronized(WebsiteConfig.class) {
				if (uniq == null) {
					uniq = new WebsiteConfig();
				}
			}
		}
		return uniq;
	}

该方式主要使用了volatile的可见性和有序性,具体见《java之volatile解析》。但是,volatile的有序性在JDK1.5中才被完全修复,在1.4及更早的Java版本中,许多JVM对volatile的实现会导致DCL失败。所以,DCL方式只能在1.5及以后的版本JDK中使用。

假如 uniq 前没有 volatile 修饰,则有序性失效,即允许编译器对代码进行重排序,那么可能会出现问题。以下列代码为例(此例引用自大众点评工程师李晓哲先生的讲座《

Java 多线程编程模式实战》PPT)。


    private static volatile WebsiteConfig instance = null;

    private int foo, bar;

    private WebsiteConfig() {
        foo = 1;
        bar = 2;
    }

    public static WebsiteConfig getInstance() {

        if (instance == null) {
            synchronized(WebsiteConfig.class) {
                if (instance == null) {
                    instance = new WebsiteConfig();
                }
            }
        }
        return instance;
    }

如下图,正常的执行顺序是1、2、3、4。

在不影响执行结果的情况下,编译器出于性能优化的考虑会对代码进行重排序。

例如重排序后的执行顺序是1、3、2、4,如果线程 B 在2执行之前访问 instance,而此时 instance 不为 null,线程 B 仍旧访问其成员变量 foo 和 bar,而此时由于 instance 尚未初始化,故 foo 和 bar 的值都是默认值 0,显然与预期不符。

单例模式_第2张图片


以上方式都有一个共同的弊端,就是上述单例类不能进行序列化,因为一旦将上述单例实例序列化到文件中,再进行反序列化成实例时,将不再满足唯一实例的条件,即可以反序列化出多个实例对象,显然与单例的初衷不符。下面介绍一种在反序列化情况下仍然满足要求的方式。

方式五:枚举方式

说到JDK1.5,Java新添加了1种类型:枚举。我们最后1中实现单例模式的方式就是枚举,先上代码:

/**
 * 
  * @ClassName  : WebsiteConfig
  * @Description: TODO
  * @author     : cmm
  * @date       : 2014-8-25 下午11:20:50
  *
 */

public enum WebsiteConfig {
	uniq;
}

关于枚举方式的好处,大家可以参考《单例模式中为什么枚举更好?》。

你可能感兴趣的:(java,设计模式)