每日设计模式之—单例模式

单例模式

单例模式:一个系统只能存在一个对象的实例,如一个国家只能有一个皇帝,一个系统只能有一个资源管理器。

单例通常分为饿汉式单例和懒汉式单例。饿汉式单例是不管需不需要,一旦加载类就进行创建,懒汉式单例是按需创建。一般来说,懒汉式单例要优于饿汉式单例

在面试过程中,面试官常常会要求我们实现一个单例模式。

单例模式的实现通常需要考虑性能和线程安全两个方面,性能高又线程安全的单例模式才是面试官青睐的单例模式。

单例模式实现通常需要以下步骤:

1.将构造函数私有化;

2.提供一个获得单例实例的公有化静态方法。

简单的单例模式实现如下:

/**
 * 懒汉式单例
 * 
 * @author Felix
 *
 */
class SingleTon1 {
	private static SingleTon1 instance;

	private SingleTon1() {

	}

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

/**
 * 饿汉式单例:线程安全的,但缺点是提前占用资源,如果单例对象本身比较大的话,会占用较多的资源,最好是能做到按需加载
 * 
 * @author Felix
 *
 */
class SingleTon2 {
	private SingleTon2() {

	}

	private static SingleTon2 instance = new SingleTon2();

	public static SingleTon2 getInstance() {
		return instance;
	}
}

虽然我们实现了两个单例类,但是,懒汉式单例并非线程安全。例如同时有两个线程A和B同时去获取SingleTon1的实例,很不幸的是当线程A进入判空条件后时间片到了,此时线程B也申请SingleTon1的实例并且成功返回,那么当线程A醒来后接着执行,又会创建一个新的对象,此时打破了系统中只能有一个实例的限制。那么该如何解决这个问题呢???

一个很自然的想法就是加锁,在getInstance()方法上加锁,每次只允许一个线程访问。于是代码变成了下边的情况。

class SingleTon3 {
	private static SingleTon3 instance;
	private static Object readOnlyObj = new Object();

	private SingleTon3() {

	}

	public static synchronized SingleTon3 getInstance() {
		if (null == instance) {
			instance = new SingleTon3();
		}
		return instance;
	}
	//或者,性能稍好于上一个方法
	public static SingleTon3 getInstance() {
		if (null==instance) {
			synchronized (readOnlyObj) {
				if (null==instance) {
					instance = new SingleTon3();
				}
			}
		}
		return instance;
	}
}

上述代码实现了线程安全的单例模式,但是每次获取实例都需要检查锁,线程之间只能串行的执行,性能较低。那么有没有既线程安全,性能有好的实现方式呢。答案是有的。且看如下实现:

class SingleTon4 {
	private SingleTon4() {

	}

	public static SingleTon4 getInstance() {
		return Helper.instance;
	}

	static class Helper {
		private static final SingleTon4 instance = new SingleTon4();
	}
}

在此处,我们使用静态内部类 的方式实现了单例模式,此单例模式具有如下特点:

1.线程安全;

2.按需加载;

3.性能好(不需要锁)

测试代码及结果如下:

public class SingleTon {
	public static void main(String[] args) {
		for(int i=0;i<1000;i++) {
			new Thread(new ThreadTest()).start();;
		}
	}
}

class ThreadTest implements Runnable {

	@Override
	public void run() {
		System.out.println(SingleTon4.getInstance());
	}

}

但是,是否我们就真的只能获取一个单例了呢???且看如下代码及运行结果:

public class SingleTon {
	public static void main(String[] args) throws ClassNotFoundException, InstantiationException,
			IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		// for(int i=0;i<1000;i++) {
		// new Thread(new ThreadTest()).start();;
		// }

		System.out.println(SingleTon4.getInstance());
		Class clzz = Class.forName("com.singleton.SingleTon4");
		System.out.println(clzz);
		Constructor<SingleTon>[] constructors = clzz.getDeclaredConstructors();
		constructors[0].setAccessible(true);
		System.out.println(constructors[0].newInstance());
	}
}

从运行结果来看,显然我们得到了两个单例对象,所以说这种方法也不能百分之百的确保只能有一个实例出现。那么,问题出在哪里呢???

回看我们的测试代码,我们通过反射首先获取到单例类的类对象clzz,然后修改了clzz对象的构造函数的访问属性,接着进行了实例化。那怎么样才能防止这种情况的出现呢??且看下边代码:

class SingleTon4 {
	private SingleTon4() {
		if (Helper.instance != null) {
			throw new RuntimeException("非法访问构造器。。。。");
		}
	}

	public static SingleTon4 getInstance() {
		return Helper.instance;
	}

	static class Helper {
		private static final SingleTon4 instance = new SingleTon4();
	}
}

我们对单例的构造函数进行了判空判断,如果不为空,说明已经存在一个实例,此时肯定是存在代码通过反射机制修改构造函数的访问属性,此操作非法,因此,只需要在构造函数里边抛出异常即可。再看测试结果:

总结:单例模式需要考虑:1.性能;2.按需加载;3.线程安全;4.防破解,最好的方式是通过内部类的方式实现(其实还可以通过枚举的方式实现),实现的时候除了私有化构造器外,还需要考虑在构造器中跑出异常,防止破解。

猜你喜欢

转载自blog.csdn.net/qq_28044241/article/details/82594101