Java设计模式-单例模式

单例模式也是23种设计模式中最常用的模式之一,在创建型模式、结构性模式和行为型模式分类中,单例模式归类为创建型模式。

单例模式确保一个类只有一个实例,并提供一个全局访问点。这种方式可以防止创建多个对象消耗过多资源,或者某种类型的对象有且只能有一个。例如创建一个对象消耗过多资源,访问IO和数据库或者进行网络传输数据等等,这时候就应该使用单例模式。

从最开始接触单例模式,包括在大多数书籍中的介绍中,懒汉式饿汉式是比较常见的两种单例模式。在本文中将会多介绍几种单例模式的创建方式,一般而言创建单例模式需要注意一下几个关键点:

  • 构造方法不对外开发,一般设置为private;
  • 通过一个静态方法或者枚举类返回单例类对象;
  • 确保单例类的对象有且只有一个,尤其在多线程环境下;
  • 确保单例类对象在反序列化时不会重新构建对象(一般很少用到);

饿汉式单例模式

public class Singleton {

	private static final Singleton INSTANCE = new Singleton();

	private Singleton() {}

	public static Singleton getInstance() {
		return INSTANCE;
	}
	
	//防止反序列化是多个实例对象
	private Object readResolve() throws ObjectStreamException {  
	  return INSTANCE;   
	 }  
	 
}

在这里final修饰符也可以剔除,饿汉式单例模式就是在类加载时就已经实例化完成,是最简单的一种实现方式,同时避免了线程同步问题。在类加载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

饿汉式单例模式的在类加载时完成实例化,所以有部分实现是在静态代码块中实现的实例化new Singleton()。

懒汉式单例模式

懒汉式(线程不安全)【不可用】

public class Singleton {
	
	private static Singleton instance;
	
	private Singleton(){}
	
	public static Singleton getInstance() {
		if(instance==null){
			instance=new Singleton();
		}
		return instance;
	}

}

相比饿汉式实现方式,这种方式虽然可以实现Lazy Loading的效果,但是在多线程并发的情况下很容易出现多个实例。我们要确保单例模式的实例在多线程的情况下也必须是有且只有一个,所以该种实现方式不可行。

懒汉式(线程安全)【不推荐用】

public class Singleton {
	
	private static Singleton instance;
	
	private Singleton(){}
	
	public static synchronized Singleton getInstance() {
		if(instance==null){
			instance=new Singleton();
		}
		return instance;
	}

}

这种方式在getInstance()方法上面加了synchronized锁,所以在多线程的情况下也是安全的。但是该种方式效率太低,每一次调用getInstance()方法都会先加锁,返回instance之后再次释放锁,需要进一步改进。

懒汉式(线程不安全)【不可用】

public class Singleton {

	private static Singleton instance;

	private Singleton() {}

	public static Singleton getInstance() {
		if (instance == null) {  //①
			synchronized (Singleton.class) {
				instance = new Singleton();
			}
		}
		return instance;
	}

}

这里使用了同步代码块,缩小了代码加锁的范围,效率提高了。但是在多线程下仍然有可能出现多个实例,如线程1和线程2代码都执行到了①,那么instance为null,这时候如果线程1先获得锁,则会创建一个新的instance,当锁释放后,线程2也会获得锁,然后又创建一个instance,所以这实现单例模式的方式不不可行的。

双重检查锁(DCL)单例【不可用】

public class Singleton {

	private static Singleton instance;

	private Singleton() {}

	public static Singleton getInstance() {
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}

}

相比较上面一种实现方式这里在同步代码块中有加上了一个是否为null的判断,直接看上去这种实现方式已经完全可以确保线程安全了。但是由于JMM的关系,当执行instance = new Singleton(),它并不是一个原子操作,这里就不细化到指令级别了,大致会做三件事:

  1. 为对象分配内存;
  2. 调用对应的构造做对象的初始化操作;
  3. 将引用instance指向新分配的空间。

这里可以看出依赖性,2依赖1,3依赖1,但是2和3之间并不存在依赖性,所以JVM会对2和3进行指令重排序。一旦按照1-3-2的执行顺序,会出现虽然返回了一个不为null的instance,但是instance中的状态确是失效的(也可以理解为instance中属性值不正确的或者仅仅赋值了默认值)。

使用volatile双重检查锁(DCL)【推荐用】

public class Singleton {

	private static volatile Singleton instance;

	private Singleton() {}

	public static Singleton getInstance() {
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}

}

这种方式使用volatile修饰了instance实例,通过这种方式就可以解决指令重排序而导致的问题了。严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取一样,具有相同的内存语义。从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语意,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。

静态内部类单例模式

public class Singleton {

	private Singleton() {}

	public static Singleton getInstance() {
		return InstanceHolder.holder;
	}

	private static class InstanceHolder {
		private static final Singleton holder = new Singleton();
	}

}

这种方式是最简单的也是最安全的实现单例模式。该方式跟饿汉式单例模式类似,但又有不同,饿汉式没有Lazy Loading的作用,但是静态内部类在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance()方法,才会装载InstanceHolder类,从而完成Singleton的实例化。所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。getInstance()方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。避免了线程不安全,延迟加载,效率高。

枚举单例模式

public enum Singleton {
	INSTANCE;
	
	public void otherMethods(){
		System.out.println("enum singleton");
	}

}

在JDK1.5及其以后才应该使用该方式,enum类型是在JDK1.5才引入的。该方式不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。但是由于我们使用的不仅仅是单例对象,访问单例对象的目的是为了使用单例中的方法,所以还要处理许多业务逻辑,在单例中定义许多方法处理业务逻辑还是比较少见,这种方式的单例模式在实际开发中还是比较少见的。

容器类单例模式

容器类单例模式在一些框架中比较常见,平常开发者使用还是比较少的。假设在一个系统中有许多管理类,而这些管理类都比较消耗资源。为了统一管理这些类,我们想单独设置一个管理这些管理类,这时候就可以使用容器类的方式设置单例模式。

public class ServiceManager {

	private static Map<String, Object> serviceMap = new HashMap<String, Object>();

	static {
		serviceMap.put(Context.WINDOW_SERVICE, new WindowManager());
		serviceMap.put(Context.WIFI_SERVICE, new WifiManager());
		serviceMap.put(Context.SENSOR_SERVICE, new SensorManager());
		serviceMap.put(Context.ALARM_SERVICE, new AlarmManager());
	}

	public Object getService(String key) {
		return serviceMap.get(key);
	}

}

这里可以发现在ServiceManager中getService()方法并不是一个static修饰的方法,当然了也可以使用静态方法,但是这里并不影响单例模式的构建,通过getService获取到的仍然是一个单例。

System.out.println(new ServiceManager().getService(Context.WINDOW_SERVICE));
System.out.println(new ServiceManager().getService(Context.WINDOW_SERVICE));
System.out.println(new ServiceManager().getService(Context.WINDOW_SERVICE));
//service.WindowManager@15db9742
//service.WindowManager@15db9742
//service.WindowManager@15db9742

容器类的单例模式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

不管以哪种方式实现单例模式,它的核心都是将构造方法私有化。究竟选择哪种方式取决于项目本身。

在Java中,全局变量基本上就是我们所说的静态变量,但是使用太多的全局变量相比单例模式也会有一些缺点,虽然单例模式也是提供一个全局的访问点。全局变量会在类加载时全部初始化,如果在某些场景下根本不会被用到,就会平白无故占用许多资源。

参考资料

单例模式的八种写法比较

常见的几种单例模式

静态内部类何时初始化

Java单例模式中双重检查锁的问题

双重检查锁失效是因为对象的初始化并非原子操作?

浅谈使用单元素的枚举类型实现单例模式

评论

您确定要删除吗?删除之后不可恢复