单例模式

[高并发Java 七] 并发设计模式中提到过单例模式,本文将详细的介绍下单例模式,方便复习查看。

单例模式的概念这里就不提了,相信绝大多数开发者都知道单例模式。

单例模式的应用:

  1. 项目中读取配置的类
  2. 网站的计数器
  3. 应用程序的日志应用
  4. 数据库连接池
  5. Application
  6. Spring中Bean的默认方式
  7. Servlet
  8. SpringMVC 控制器对象

单例模式的优点:

由于单例模式只生成一个实例,减少了系统的开销。

本文主要介绍5种单例模式的实现:

  1. 饿汉式(线程安全,调用效率高,但是不能延迟加载)
  2. 懒汉式(线程安全,调用效率不高,可以延迟加载)
  3. 双重检测锁式(由于JVM底层内部模型原因,偶尔会出现问题,不建议使用)
  4. 静态内部类式(线程安全,调用效率高,可以延迟加载
  5. 枚举单例线程安全,调用效率高,不能延迟加载

1. 饿汉式

public class Singleton {
    private Singleton() {
        System.out.println("Singleton is create");
    }
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
}
static会在类加载时初始化,此时不会发生多个对象线程不安全问题,虚拟机保证只会加载一次该类,肯定不会发生并发访问问题,因此,可以省略synchronized关键字。

问题:如果只是加载本类,没有调用getInstance(),甚至永远都没有调用。但是在类记载时依然初始化了对象,造成了资源浪费。

由于不需要使用synchronized,调用效率高

2. 懒汉式

public class Singleton {
    private Singleton() {
        System.out.println("Singleton is create");
    }
    private static Singleton instance = null;
    public static synchronized Singleton getInstance() {
        if (instance == null)
            instance = new Singleton();
        return instance;
    }
}
延迟加载,真正用的时候才加载。

问题:资源利用率高了,但是每次调用getInstance()方法都要同步,并发效率低。

3. 双重检测锁式

public class Singleton
{
	private volatile static Singleton instance = null;
	private Singleton()
	{
	}
	public static Singleton getInstance()
	{
		if (instance == null)
		{
			synchronized (Singleton.class)//1
			{
				if (instance == null)//2
				{
					instance = new Singleton();// 3
				}
			}
		}
		return instance;
	}
}

这个模式将同步内容下放到if内部,提高了执行效率。不必每次获取对象时都同步,只有第一次才同步,创建后就没必要了。

问题:由于JVM底层内部模型原因,偶尔会出现问题,不建议使用。

为处理非延迟加载方式瓶颈问题,我们需要对instance进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同获取锁了),但在Java中行不通,因为同步块外面的if (instance == null)可能看到已存在,但不完整的实例。JDK5.0以后版本若instance为volatile则可行。

3.1 无序性

为解释该问题,需要重新查看上述清单中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 instance 来引用此对象。这行代码的问题是:在 Singleton 构造函数体执行之前,变量 instance 可能成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程得到的是一个还会初始化的对象,这样会导致系统崩溃。
什么?这一说法可能让您始料未及,但事实确实如此。在解释这个现象如何发生前,请先暂时接受这一事实,我们先来看一下双重检查锁定是如何被破坏的。假设代码执行以下事件序列:

1、线程 1 进入 getInstance() 方法。
2、由于 instance 为 null,线程 1 在 //1 处进入 synchronized 块。 
3、线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非 null。 
4、线程 1 被线程 2 预占。
5、线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将 instance 引用返回给一个构造完整但部分初始化了的 Singleton 对象。 
6、线程 2 被线程 1 预占。
7、线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。
为展示此事件的发生情况,假设代码行 instance =new Singleton(); 执行了下列伪代码:
mem = allocate();             //为单例对象分配内存空间.
instance = mem;               //注意,instance 引用现在是非空,但还未初始化
ctorSingleton(instance);    //为单例对象通过instance调用构造函数
这段伪代码不仅是可能的,而且是一些 JIT 编译器上真实发生的。执行的顺序是颠倒的,但鉴于当前的内存模型,这也是允许发生的。JIT 编译器的这一行为使双重检查锁定的问题只不过是一次学术实践而已。

文章http://dev.csdn.net/author/axman/4c46d233b388419e9d8b025a3c507b17.html中提到,在JDK1.2以后就不会发生这个问题。

不过为了确保有序性,应该使用volatile来修饰instance变量,因为Happen-Before原则确保了,volatile变量的写先发生于读,保证了volatile变量的可见性。关于无序性原因和Happen-Before原则请查看[高并发Java 三] Java内存模型和线程安全

3.2 使用ThreadLocal来解决双重检测问题

public class Singleton
{
	private static final ThreadLocal perThreadInstance = new ThreadLocal();
	private static Singleton singleton;

	private Singleton()
	{
	}

	public static Singleton getInstance()
	{
		if (perThreadInstance.get() == null)
		{
			// 每个线程第一次都会调用
			createInstance();
		}
		return singleton;
	}

	private static final void createInstance()
	{
		synchronized (Singleton.class)
		{
			if (singleton == null)
			{
				singleton = new Singleton();
			}
		}
		perThreadInstance.set(perThreadInstance);
	}
}

借助于ThreadLocal,将临界资源(需要同步的资源)线程局部化,具体到本例就是将双重检测的第一层检测条件 if (instance == null) 转换为了线程局部范围内来作。这里的ThreadLocal也只是用作标示而已,用来标示每个线程是否已访问过,如果访问过,则不再需要走同步块,这样就提高了一定的效率。但是ThreadLocal在1.4以前的版本都较慢,但这与volatile相比却是安全的。

4. 静态内部类式

public class Singleton
{
	private Singleton()
	{
		System.out.println("StaticSingleton is create");
	}

	private static class SingletonHolder
	{
		private static Singleton instance = new Singleton();
	}

	public static Singleton getInstance()
	{
		return SingletonHolder.instance;
	}
}
由于加载一个类时,其内部类不会被加载。这样保证了只有调用getInstance()时才会产生实例,控制了生成实例的时间,实现了延迟加载。

并且去掉了synchronized,让性能更优,用static来确保唯一性。

兼备了并发高效调用和延迟加载的优势。

5. 枚举式

public enum Singleton
{
	INSTANCE;
	
	public void someMethod()
	{
		
	}
}

实现简单,枚举本身就是单例模式,由JVM从根本上提供了保障,避免提供反射和反序列化的漏洞

枚举类中的枚举值,本身就是枚举类的实例,并且实例数量在运行时就已经确定了,无法改变,所以只有一个枚举值的枚举类,必然是单例的。

枚举类的性能也不错,唯一的缺点就是不能延迟加载。

问题:无延迟加载

6. 反射破坏单例模式

import java.lang.reflect.Constructor;


public class Client
{
	public static void main(String[] args) throws Exception
	{
		Singleton s1 = Singleton.getInstance();
		Singleton s2 = Singleton.getInstance();
		System.out.println(s1 == s2);
		
		Constructor constructor = Singleton.class.getDeclaredConstructor();
		constructor.setAccessible(true);
		Singleton s3 = (Singleton)constructor.newInstance();
		Singleton s4 = (Singleton)constructor.newInstance();
		System.out.println(s1);
		System.out.println(s2);
		System.out.println(s3);
		System.out.println(s4);
	}

}
使用反射可以跳过权限检查去生成多个实例。

当然我们可以在写单例模式的时候避免反射破坏单例:

private Singleton()
{
	if(instance != null)
	{
		throw new RuntimeException();
	}
}

7. 反序列破坏单例模式

做法就是,将实例序列化到磁盘,再反序列化出来,将得到不同的两个实例。

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Client
{
	public static void main(String[] args) throws Exception
	{
		Singleton s1 = Singleton.getInstance();
		Singleton s2 = Singleton.getInstance();
		System.out.println(s1 == s2);
		
		FileOutputStream fos = new FileOutputStream("f:/aa");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(s1);
		oos.close();
		fos.close();
		
		FileInputStream fis = new FileInputStream("f:/aa");
		ObjectInputStream ois = new ObjectInputStream(fis);
		Singleton readObject =(Singleton)ois.readObject();
		System.out.println(s1);
		System.out.println(s2);
		System.out.println(readObject);
	}

}

预防措施:

由于序列化需要实现Serializable,当然可以直接让单例不实现Serializable。

如果这个单例必须要实现Serializable,需要在单例类中实现:

private Object readResolve() throws ObjectStreamException
{
	return instance;
}
反序列化时,如果定义了readResolve方法,则直接返回此方法指定的对象,不需要再单独创建对象。

8. 比较单例模式的效率

import java.util.concurrent.CountDownLatch;

public class Client
{
	public static void main(String[] args) throws Exception
	{
		final CountDownLatch cd = new CountDownLatch(10);
		long start = System.currentTimeMillis();
		for (int i = 0; i < 10; i++)
		{
			new Thread(new Runnable()
			{
				@Override
				public void run()
				{
					for (int j = 0; j < 100000; j++)
					{
						Singleton singleton = Singleton.getInstance();
					}
					cd.countDown();
				}
			}).start();
		}
		cd.await();
		long end = System.currentTimeMillis();
		System.out.println(end - start);
	}

}
在多线程环境下测试各个单例模式的效率

一般来说,懒汉式效率是要比其他模式低两个数量级的。

如果不需要延迟加载

枚举式好于饿汉式

如果需要延迟加载

静态内部类式好于懒汉式

Reference:

1. http://jiangzhengjun.iteye.com/blog/652440

你可能感兴趣的:(枚举,单例模式,饿汉式,懒汉式,双重检测锁式,静态内部类式)