单例模式

南笙酒味 提交于 2020-01-27 04:52:14

单例模式是一种比较常见的设计模式,但是在Java中要用好单例模式,并不是一件简单的事。在整个系统中,单例类只能有一个实例对象,且需要自行完成示例,并始终对外提供同一实例对象。

因为单例模式只允许创建一个单例类的实例对象,避免了频繁地创建对象,所以可以减少GC的次数,也比较节省内存资源,加快对象访问速度。例如,数据库连接池、应用配置等一般都是单例的。

单例模式有很多种写法,但是有些写法在特定的场景下,尤其是多线程条件下,无法满足实现单一实例对象的要求, 从而导致错误。首先我们来介绍比较经典的懒汉模式和饿汉模式,具体实现如下:

//饿汉模式
public class Singleton{
	//饿汉模式是最简单的实现方式,在类加载的时候就创建了单例类的对象
	private static final Singleton instance = new Singleton();
	
	//单例类的构造方法都是私有的,防止外部创建单例类的对象
	private Singleton();
	
	public static Singleton newInstance(){
		return instance;//返回唯一的到单例对象
	}
}

//懒汉模式
public class Singleton{
	private static Singleton instance = null;
	
	private Singleton();
	
	public static Singleton newInstance(){
		//在需要的时候才去创建单例对象,如果单例对象已经创建,再次调用newTnstance()方法时
		//将不会重新创建新的单例对象,而是直接返回之前创建的单例对象
		if(null == instance){
			instance = new Singleton();
		}
		return instance;
	}
}

懒汉模式有延迟加载的意思,如果创建单例对象会消耗大量资源的情况下,在真正使用单例对象时创建其实例是- - -个不错的选择。但是懒汉模式有一个明显的问题,就是没有考虑线程安全的问题,在多线程的场景中,可能会有多个线程并发调用newInstance()方法创建单例对象,从而导致系统中同时存在多个单例类的实例,这显然不符合需求。我们可以通过给newInstance()方法加锁解决该问题,得到如下代码:

//懒汉模式
public class Singleton{
	private static Singleton instance = null;
	
	private Singleton();
	
	//使用synchronized为newInstance()方法加锁
	public static synchronized Singleton newInstance(){
		if(null == instance){
			instance = new Singleton();
		}
		return instance;
	}
}

虽然这种修改方式可以保证线程安全,但是每次访问newInstance()方法时, 都会进行一次加锁和解锁操作,而单例类是全局唯的,该锁就可能成为系统的瓶颈。为了解决问题,可以就有人提出了“双重检查锁定"的方式,但请读者注意,这是错误的写法,具体代码如下:

public class Singleton{
	private static Singleton instance = null;
	
	private Singleton();
	
	//使用synchronized为newInstance()方法加锁
	public static synchronized Singleton newInstance(){
		if(null == instance){//第一次检测
			synchronized(Singleton.class){//加锁
				if(null == instance){//第二次检测
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

由于指令重排优化,可能会导致初始化单例对象和将该对象地址赋值给instance字段的顺序与上面Java代码中书写的顺序不同。例如,线程A在创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时线程A就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。线程B来调用newInstance(方法,得到的就是未初始化完全的单例对象,这就会导致系统出现异常行为。

为了解决该问题,我们可以使用volatile 关键字修饰instance字段。volatile 关键字的一个语义就是禁止指令的重排序优化,从而保证instance字段被初始化时,单例对象已经被完全初始化。最终得到的代码如下所示。

public class Singleton{
	//使用volatile关键字修饰instance字段
	private static volatile Singleton instance = null;
	
	private Singleton();
	
	public static Singleton newInstance(){
		if(null == instance){//依旧是双重检测		
			synchronized(Singleton.class){
				if(null == instance){
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

相对于双重检测 的写法,推荐使用静态内部类的单例模式写法,这种写法也可以实现延迟加载的相关,且通过类加载机制保证只创建一个单例对象,具体写法如下:

public class Singleton{
	//私有的静态内部类,该静态内部类只会在newInstance()方法中被使用
	private static class SingletonHolder{
		//静态字段
		public static Singleton instance = new Singleton();
	}
	
	private Singleton();
	
	public static Singleton newInstance(){
		return SingletonHolder.instance;//访问静态内部类中的静态方法
	}
}

熟悉Java类加载机制的读者知道,当第一次访问类中的静态字段时;会触发类加载,并且同一个类只加载一次。静态内部类也是如此,类加载过程由类加载器负责加锁,从而保证线程安全。这种写法相比较于创双重检测锁的方法,更加简洁明了,更不容易出错。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!