Fork me on GitHub

单例模式 - 简不简单你说了算?

很多设计模式系列的文章开篇大都以 { 单例模式 } 小试牛刀,标明了单例是最简单的设计模式之一,我也循业内规则,说一说简单的单例模式。

它真的简单吗?

      此为单例模式的类图:一个普通类 Singleton,类中包含了一个 Singleton 类型的私有静态字段 instance、受保护的构造器以及一个返回值为 Singleton 的公有静态方法 getInstance() 。so easy?tmp56EB

先来实现一个简单的“经典“单例模式(为什么打上引号,你懂的!):

public class ClassicSingleton {
	private static ClassicSingleton instance = null;

	protected ClassicSingleton() {

	}

	public static ClassicSingleton getInstance() {
		if (instance == null) {
			instance = new ClassicSingleton();
		}
		return instance;
	}
}

如上代码若是在面试中出现,那么面试官的第一反映就是:这个小伙要学的东西还很多啊!too young too simple!热心点的面试官可能会以各种反问句的方式来解释此段代码的各种坑,不耐烦的可能就直接 pass 了……

说好的私有构造器呢?

      在 Java 语言规范中,protected 的构造方法能够被其子类以及在同一个包中的其它类调用来实例化类,由此何谈类的单实例!这个坑还比较好填,修改单例类构造器为 private,如此构造器只能在单例类内部调用,子类以及同一个包中的其它类便无法调用,保证了全局只能通过调用getInstance 方法这一种途径来获取单例类的实例。Better better,best!此种情形下如果声明类为 final 类,这样意图明确且采用了编译器的某些性能优化,何乐而不为!2015-10-30_115543

多线程因素的考虑

      经过如上修改后的单例已初具雏形,但是在多线程环境下还存在线程安全问题。为什么这么说呢?来看上图中灰色部分的 if 语句块,这段代码对 instance 这个临界资源进行读写访问,如果此时存在两个线程,线程 1 通过了 if 判断进入语句块中准备对 instance 赋值,恰巧此时发生线程切换(instance 还没赋上值仍然是 null),调度程序调度线程 2 执行这段指令,因为 instance 此时仍为空,那么它也通过了 if 判断进到了语句块,之后的结果便是线程 1 和 2 分别实例化了一个单列类。这样的结果可不是我们希望看到的!image

对于多线程程序的测试也得经过一番倒腾,来看下面的代码段:

第一步:对 Singleton 类进行修改,加入 simulateRandomActivity(),模拟将进入 if 语句块的线程 1 先人工睡眠 50ms 以使线程 2 有足够时间去处理 if 判断

public final class Singleton {
	private static Singleton instance = null;

	private static Logger logger = Logger.getRootLogger();
	private static boolean firstThread = true;

	private Singleton() {
		logger.info("----->singleton's .ctor...");
	}

	public static Singleton getInstance() {
		if (instance == null) {
			simulateRandomActivity();
			instance = new Singleton();
		}
		logger.info("created singleton: " + instance);
		return instance;
	}

	private static void simulateRandomActivity() {
		try {
			if (firstThread) {
				firstThread = false;
				logger.info("first thread sleeping...");
				// 让线程 1 睡眠 50ms 使线程 2 有足够时间走到 if 判断
				Thread.currentThread().sleep(50);
			}
		} catch (InterruptedException ex) {
			logger.warn("Sleep interrupted");
		}
	}
}

第二步:编写测试案例,模拟两个线程同步调用单例类的 getInstance() 方法

public class SingletonTest extends TestCase {
	private static Singleton singleton = null;

	public SingletonTest(String name) {
		super(name);
	}

	public void setUp() {
		singleton = null;
	}

	public void testUnique() throws InterruptedException {
		Thread threadOne = new Thread(new SingletonTestRunnable()), 
				threadTwo = new Thread(new SingletonTestRunnable());
		threadOne.start();
		threadTwo.start();

		threadOne.join();
		threadTwo.join();
	}

	private static class SingletonTestRunnable implements Runnable {
		public void run() {
			Singleton instance = Singleton.getInstance();

			// 同步两个测试线程 保证对 singleton 变量的访问线程安全
			synchronized (SingletonTest.class) {
				if (singleton == null)
					singleton = instance;
			}
			Assert.assertEquals(true, instance == singleton);
		}
	}
}

如上测试类的运行结果如下:

image

同步

      要解决如上描述的线程安全问题,就要学习线程的同步机制,本篇重点不在此就不赘述了,详见“并发编程”博文系列。Java在更高的层次上封装了管程和锁的思想,提供了 synchronized 关键字用于解决线程同步。那么对代码进行如下修改便能够解决如上创建多个单例的情况:

public synchronized static Singleton getInstance() {
	if (instance == null) {
		simulateRandomActivity();
		instance = new Singleton();
	}
	logger.info("created singleton: " + instance);
	return instance;
}

在同步化 getInstance 方法之后运行测试案例可以获得运行结果:image

问题解决了吗?

      synchronized 关键字在解决线程安全问题是可以说是“万能”的,大部分的并发控制操作都能使用 synchronized 来完成。但是越是“万能”的并发控制,通常也会伴随着越大的性能影响。那么我们就想着缩小临界区,让程序发生同步的地方尽量减少,可以在一定程度上减少性能消耗。一种性能改进的方法如下:

public static Singleton getInstance() {
	if (instance == null) {
		synchronized (Singleton.class) {
			simulateRandomActivity();
			instance = new Singleton();
		}
	}
	logger.info("created singleton: " + instance);
	return instance;
}

这个代码片段只同步关键代码,而不是同步整个方法,缩小了临界区。我们理想中是想着 getInstance 方法只需要在第一次调用的时候需要线程同步,这样可以很大程度优越于同步整个方法。但是理想是美好的,现实是骨感的!这段代码和没有同步之前存在同样的线程安全问题,聪明的你一定看出关键点就是在那个 if 判断。当有两个线程同时进到 if 块中还是得创建两个单例,问题还是没解决啊!

双重加锁检查

public static Singleton getInstance() {
	if (instance == null) {
		synchronized (Singleton.class) {
			if (instance == null) {
				simulateRandomActivity();
				instance = new Singleton();
			}
		}
	}
	logger.info("created singleton: " + instance);
	return instance;
}

此时就算有两个线程恰巧同时进到 if 块中,由于同步块的存在,线程得排队去执行临界区的代码,那么在第一个线程执行完实例化的语句后,instance便不为空,待线程调度到第二个线程执行时他会先执行判断,发现 instance 不为空直接过去了,保证了只实例化一次!这种技术就是著名的双检锁技术

至此线程因素的考虑得到完满解决。

三行代码给你个单例模式!

三行代码给出的一种实现单例的技术,俗称饿汉式!城会玩!

public final class HungerSingleton {
	public final static HungerSingleton INSTANCE = new HungerSingleton();
	private HungerSingleton() {
	}
}
三行代码的单例想想也挺极端的,把所有的变化都写死了!来看看更折中一点的实现:
public final class HungerSingleton {
	private static HungerSingleton INSTANCE = new HungerSingleton();

	private HungerSingleton() {
	}

	public static HungerSingleton getInstance() {
		return INSTANCE;
	}
}

通过 getInstance 方法返回单例,可以通过改变这个方法来增加一些可变因素,灵活了许多。

这种饿汉式简单、快速,而且通过 classloader 机制也避免了多线程的同步问题。不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多,在单例模式中大多是调用 getInstance 方法导致的(不排除有其他静态方法),此时初始化 instance 就没有了上面那种懒汉式单例的延迟加载(lazy loading)的效果。这种方式只要 HungerSingleton 类被装载,那么 instance 就会被实例化,试想如果实例化 instance 很耗费资源,我想让他延迟加载,或者说我不想在 HungerSingleton 类一加载的时候就实例化 instance,因为我不能确保 HungerSingleton 类还可能在其它地方被主动使用从而被加载,这个时候实例化 instance 显然是不合理的。来看一种利用静态内部类技术实现的折中方案:

public final class HungerSingleton {
	private static class SingletonHolder {
		private static final HungerSingleton INSTANCE = new HungerSingleton();
	}

	private HungerSingleton() {
	}

	public static final HungerSingleton getInstance() {
		return SingletonHolder.INSTANCE;
	}
}

这时即使 HungerSingleton 类被加载了,INSTANCE 也不一定被实例化,因为 SingletonHolder 类没有被主动使用,只有显示通过调用 getInstance 方法时才会显示装载 SingletonHolder 类,从而实例化 INSTANCE,达到 lazy loading 效果。

多 classloader 对单例模式的影响

对于任意一个类,都需要由它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,而使用多个类加载器是很普遍的,所以不管你在实现单例类时多么小心你都最终可以得到多个单例类的实例。来看一个自定义类加载器加载类的示例:

public class ClassLoaderTest {
	public static void main(String[] args) throws Exception {

		ClassLoader myLoader = new ClassLoader() {
			@Override
			public Class<?> loadClass(String name) throws ClassNotFoundException {
				try {
					String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
					InputStream is = getClass().getResourceAsStream(fileName);
					if (is == null) {
						return super.loadClass(name);
					}
					byte[] b = new byte[is.available()];
					is.read(b);
					return defineClass(name, b, 0, b.length);
				} catch (IOException e) {
					throw new ClassNotFoundException(name);
				}
			}
		};
		// 由系统应用程序类加载器加载
		Singleton instance = Singleton.getInstance();
		// 自定义的类加载器加载
		Class<?> clazz = myLoader.loadClass("com.saga.patterns.singleton.Singleton");
		Constructor<?> constructor = clazz.getDeclaredConstructor();
		constructor.setAccessible(true);
		Object obj = constructor.newInstance();

		System.out.println(obj.getClass());
		// false
		System.out.println(obj == instance);
	}
}

image

从输出结果中看出,第一个 Singleton 是有调用 getInstance 方法由系统类加载器加载实例化出来的对象,而第二个 Singleton 实例是由我们自己实现的类加载器加载的,做对象相等判断的时候结果是 false。所以虽然它们都来自同一个 Class 文件,但在程序中依然是两个独立的类。

解决由此引发的单例问题,可以使用加载单例类基类的那个类加载器:

private static Class<?> getClass(String classname) throws ClassNotFoundException {
	ClassLoader loader = Thread.currentThread().getContextClassLoader();
	if (loader == null) {
		loader = Singleton.class.getClassLoader();
	}
	return (loader.loadClass(classname));
}

反射破坏单例原则

在单例模式中,我们只对外提供工厂方法(获取单例),而私有化构造函数,来防止外面多余的创建。对于一般的外部调用来说,私有构造函数已经很安全了。但是一些特权用户可以通过反射来访问私有构造函数,把访问权限打开 setAccessible(true),就可以访问私有构造函数了,这样破坏了单例的私有构造函数保护,实例化了一个新的实例。如果要防御这样的反射侵入,可以修改构造函数,加上第二次实例化的检查,当发生创建第二个单例的请求时会抛出异常。

private static int cntInstance = 0;

private Singleton() throws Exception {
	if (cntInstance++ > 1) {
		throw new Exception("can't create another singleton instance.");
	}
}

序列化的陷阱

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

public final class SerializableSingleton implements Serializable {

	private static final long serialVersionUID = 1L;
	private static SerializableSingleton instance = new SerializableSingleton();

	private SerializableSingleton() {

	}

	public static SerializableSingleton getInstance() {
		return instance;
	}

	private Object readResolve() {
		return instance;
	}
}

为上面的可序列化单例类写个测试案例,检查被重构的单例类实例是否同一个对象:

public class SerializableSingletonTest extends TestCase {
	private SerializableSingleton sone = null, stwo = null;
	private static final Logger logger = Logger.getRootLogger();

	public SerializableSingletonTest(String name) {
		super(name);
	}

	@Override
	protected void setUp() throws Exception {
		sone = SerializableSingleton.getInstance();
		stwo = SerializableSingleton.getInstance();
	}

	public void testSerialize() {
		logger.info("testing singleton serialization...");
		writeSingleton();
		SerializableSingleton s1 = readSingleton();
		SerializableSingleton s2 = readSingleton();
		Assert.assertEquals(true, s1 == s2);
	}

	private void writeSingleton() {
		try {
			FileOutputStream fos = new FileOutputStream("serializedSingleton");
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			SerializableSingleton s = SerializableSingleton.getInstance();

			oos.writeObject(SerializableSingleton.getInstance());
			oos.flush();
		} catch (NotSerializableException se) {
			logger.fatal("Not Serializable Exception: " + se.getMessage());
		} catch (IOException iox) {
			logger.fatal("IO Exception: " + iox.getMessage());
		}
	}

	private SerializableSingleton readSingleton() {
		SerializableSingleton s = null;

		try {
			FileInputStream fis = new FileInputStream("serializedSingleton");
			ObjectInputStream ois = new ObjectInputStream(fis);
			s = (SerializableSingleton) ois.readObject();
		} catch (ClassNotFoundException cnf) {
			logger.fatal("Class Not Found Exception: " + cnf.getMessage());
		} catch (NotSerializableException se) {
			logger.fatal("Not Serializable Exception: " + se.getMessage());
		} catch (IOException iox) {
			logger.fatal("IO Exception: " + iox.getMessage());
		}
		return s;
	}

	public void testUnique() {
		logger.info("checking singletons for equality");
		Assert.assertEquals(true, sone == stwo);
	}
}

image

重磅推出:Effective Java 推荐使用枚举实现单例

关于单例模式,以上讨论了饿汉式、懒汉式、借助内部类、双检锁等都可以实现,这些实现可以保证线程安全,但是在某些特殊情况下并不能够保证仅仅只有一个单例,像序列化、反射攻击、多个类加载器等往往可以生成新的实例对象,对此 Effective Java 书中推荐使用枚举单例。

public enum EnumSingleton {
	INSTANCE {
		@Override
		protected void read() {
			System.out.println("read...");
		}

		@Override
		protected void write() {
			System.out.println("write...");
		}
	};
	protected abstract void read();

	protected abstract void write();
}

以上是一个枚举单例的例子,通过 EnumSingleton.INSTANCE 获取实例,此种方式可以保证单例线程安全、防反射攻击、防止序列化生成新实例。知其然,亦要知其所以然!JVM为我们实现了很多东西,来看看上面代码反编译的结果:

public abstract class EnumSingleton extends Enum
{

    private EnumSingleton(String s, int i)
    {
        super(s, i);
    }

    protected abstract void read();

    protected abstract void write();

    public static EnumSingleton[] values()
    {
    	EnumSingleton asingleton[];
        int i;
        EnumSingleton asingleton1[];
        System.arraycopy(asingleton = ENUM$VALUES, 0, asingleton1 = new EnumSingleton[i = asingleton.length], 0, i);
        return asingleton1;
    }

    public static EnumSingleton valueOf(String s)
    {
        return (EnumSingleton)Enum.valueOf(singleton/EnumSingleton, s);
    }

    EnumSingleton(String s, int i, EnumSingleton singleton)
    {
        this(s, i);
    }

    public static final EnumSingleton INSTANCE;
    private static final EnumSingleton ENUM$VALUES[];

    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0) {

            protected void read()
            {
                System.out.println("read");
            }

            protected void write()
            {
                System.out.println("write");
            }

        };
        ENUM$VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}
  • 关于防反射攻击,注意看 EnumSingleton 类的修饰 abstract,所以没法实例化,反射也无能为力
  • 关于线程安全,枚举单例其实是通过类加载机制来保证,注意看 INSTANCE 的实例化时机,是在 static 块中
  • 关于防止序列化生成新的实例,Java 对 Enum 类型的对象序列化和其它类型的对象序列化有所不同,在序列化的时候 Java 仅仅是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf 方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

对单例模式垃圾回收问题的讨论

Java1.2 以后单例是不会被回收的!详见 http://blog.csdn.net/zhengzhb/article/details/7331354

续:使用 ThreadLocal 为每个线程生成一个不同的单例副本

 

单例模式简单却容易让人迷惑,特别是对于Java的开发者来说,我觉得它不简单!您认为呢?

posted @ 2015-10-30 11:50  Time2Goo  阅读(447)  评论(0编辑  收藏  举报