23种设计模式之单例模式(java实现)

一、定义及其优缺点

单例模式:整个程序有且仅有一个实例;该类负责创建自己的对象,同时确保只有一个对象被创建。

一般实现单例模式需要以下几点:

  1. 类构造器私有(一般为无参构造)
  2. 持有自己类型的私有属性(private static Singleton instance;)
  3. 对外提供获取实例的静态方法(public static Singleton  getInstance())

优点:

  • 单例模式在内存中只有一个实例,减少内存开支。(特别是一个对象需要频繁地创建销毁时)
  • 单例模式只生成一个实例,减少系统的性能开销。(当一个对象产生需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决)
  • 单例模式可以避免对资源的多重占用。(例如一个写文件操作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作)
  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问。(例如,可以设计一个单例类,负责所有数据表的映射处理)

缺点:

  • 单例模式没有抽象层,扩展很困难;若要扩展,除了修改代码基本上没有第二种途径可以实现。
  • 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
  • 滥用单例将带来一些负面问题:如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

 二、单例模式的几种实现及其优缺点

1.饿汉模式:单例对象非延迟加载,线程安全,比较常用,但容易产生垃圾,因为一开始就初始化

public class HungryManSingleton {
	//1、私有化构造器
	private HungryManSingleton() {
	}	
	//2、持有自己类型的私有属性(类加载时,天然的线程安全,类初始化时,立即加载这个对象(唯一一个),缺乏延时加载的优势)
	private static HungryManSingleton instance = new HungryManSingleton();
	//3、暴露创建对象的方法(方法没有加同步,因此调用效率高)
	public static HungryManSingleton getInstance() {
		return instance;
	}
}

2.懒汉模式(线程安全模式):实现延迟加载(懒加载)、线程安全,但效率低,每一次生成实例都要同步

public class LazyManSingleton {
	//1、私有化构造器
	private LazyManSingleton() {
	}	
	//2、持有自己类型的私有属性,类初始化时,不初始化这个对象(延迟加载,真正用时再创建)
	private static LazyManSingleton instance;
	//3、暴露创建对象的方法(方法同步,因此调用效率低)
	public static synchronized LazyManSingleton getInstance() {
		if(null == instance) {
			instance = new LazyManSingleton();
		}
		return instance;
	}
}

3.双重检查锁模式:线程安全,延迟初始化,效率高

public class DoubleCheckSingleton {
	// 私有化构造器
	private DoubleCheckSingleton() {}
	// volatile关键字禁止指令重排
	private volatile static DoubleCheckSingleton instance;
	// 双重检查锁
	public static DoubleCheckSingleton getInstance() {
		if (instance == null) {
			synchronized (DoubleCheckSingleton.class) {
				if (instance == null) {
					instance = new DoubleCheckSingleton();
				}
			}
		}
		return instance;
	}
}

双重检查锁模式在创建实例的时候进行了双重判断,第一层判断是为了避免已经存在实例时,再次进入同步,由于避免了除第一次建立实例的同步操作,提升了效率;第二层判断是为了保证多线程下的同步操作,防止创建多个实例。

由于JVM内部的优化机制,有可能导致 instance = new DoubleCheckSingleton() 对象在创建时发生指令重排(对象创建过程很复杂,JVM会对其创建过程进行指令重排,导致提前释放锁),因此多线程下该模式存在风险;volatile关键字禁止指令重排,解决该问题。

4.静态内部类实现单例模式:线程安全、调用效率高、延迟加载(好于懒汉模式,可以优先选择)

public class StaticInnerClassSingleton {
	//1、私有化构造器
	private StaticInnerClassSingleton() {}	
	//2、静态内部类(延迟加载)
	private static class Singleton{
		private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
	}
	//3、暴露创建对象的方法,直接返回内部类的对象(内部类加载,天然线程安全)
	public static StaticInnerClassSingleton getInstance() {
		return Singleton.instance;
	}
}

5.枚举实现单例模式:线程安全、调用效率高、避免了反射与反序列化的漏洞(非延迟加载,好于饿汉式)

public enum EnumSingleton {
	//枚举元素,本身就是单例对象
	INSTANCE;
	//操作枚举元素即可
	public void singletonOperation() {}
}

三、防止反射和反序列化破解单例模式(枚举天然防护此漏洞)

1.反射可以通过constructor.setAccessible(true);的方式,跳过private的防护,调用私有构造器,因此可以利用反射生成新的实例。为防止其破解单例模式,可以在构造器那里加以限制,代码如下:

public class LazyManSingletonCrack {
	//1、私有化构造器
	private LazyManSingletonCrack() {
		//防止反射破解单例模式
		if (instance != null) {    
                //若已经创建实例,主动抛出异常
			throw new RuntimeException();
		}
	}	
	//2、持有自己类型的私有属性,类初始化时,不初始化这个对象(延迟加载,真正用时再创建)
	private static LazyManSingletonCrack instance;
	//3、暴露创建对象的方法(方法同步,因此调用效率低)
	public static synchronized LazyManSingletonCrack getInstance() {
		if(null == instance) {
			instance = new LazyManSingletonCrack();
		}
		return instance;
	}
}

2.反序列化时,会创建一个新的实例接收原来序列化后写到硬盘中的实例,从此破解单例模式;为防止其进行破解,可以在单例模式中定义readResolve()方法,让实例保持唯一,代码如下:

public class LazyManSingletonCrack implements Serializable{
	//1、私有化构造器
	private LazyManSingletonCrack() {}	
	//2、持有自己类型的私有属性
	private static LazyManSingletonCrack instance;
	//3、暴露创建对象的方法
	public static synchronized LazyManSingletonCrack getInstance() {
		if(null == instance) {
			instance = new LazyManSingletonCrack();
		}
		return instance;
	}
	//反序列化时,如果定义了readResolve,可以将指定对象返回,不会返回反序列化后的新对象
	private Object readResolve() throws ObjectStreamException{
		return instance;
	}
}

四、测试各种单例模式的性能

1.多线程环境下测试不同单例模式的效率,在本机运行环境下得到测试结果,测试代码如下

public class TestEfficiency {
	public static void main(String[] args) throws Exception {
		long start = System.currentTimeMillis();
		int threadNum = 10;//线程数量
		//为了保证线程都执行完,再计算时间,使用线程计数器CountDownLatch,内部类不能调用局部变量,因此声明为final常量
		final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
		//new threadNum 个线程
		for (int i = 0; i < threadNum; i++) {
			//内部类方式实现多线程
			new Thread(new Runnable() {
				@Override
				public void run() {
					//循环创建100000次
					for (int i = 0; i < 1000000; i++) {
						Object obj = LazyManSingleton.getInstance();
					}
					//线程结束后计数器减一
					countDownLatch.countDown();
				}
			}).start();
		}
		//main线程进行等待(阻塞),直到所有线程结束
		countDownLatch.await();
		long end = System.currentTimeMillis();
		System.out.println(end - start);
	}
}

2.多线程环境下测试不同单例模式的效率,测试结果如下

饿汉式 25ms
懒汉式 139ms
双重检查锁 46ms
静态内部类 31ms
枚举 39ms

猜你喜欢

转载自blog.csdn.net/Jeff_fei/article/details/103332953