什么是单例模式,单例模式(Singleton)又叫单态模式,他出现的目的是为了保证一个类在系统中只有一个实例,并提供一个访问它的全局访问点。
许多时候整个系统只需要拥有⼀个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在⼀个文件中,这些配置数据由⼀个单例对象统⼀读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种⽅式简化了在复杂环境下的配置管理。
基本代码:
public class DataSourceSingleton {
// 1.提供私有的构造方法
private DataSourceSingleton() {
}
// 2. 创建一个私有的属性对象
private static DataSourceSingleton dataSource = new DataSourceSingleton();
// 3.提供公共的对外的单例对象
public static DataSourceSingleton getInstance() {
return dataSource;
}
}
在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
简单来说:
我们直到全局变量分为静态变量和实例变量,静态变量也可以保证该类的实例只存在一个。只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。
但是,如果说这个对象非常消耗资源,而且程序某次的执行中一直没用,这样就造成了资源的浪费。例如单例模式的话,我们就可以实现在需要使用时才创建对象,这样就避免了不必要的资源浪费。不仅仅是因为这个原因,在程序中我们要尽量避免全局变量的使用,大量使用全局变量给程序调试、维护等带来困难。
所谓 “懒汉方式” 就是说单例模式再第一次使用时创建,而不是在JVM在加载这个类时马上创建此唯一的单例实现
为什么叫懒汉?
因为懒汉懒惰,懒得初始化,用到了才开始初始化。
这也就会造成线程安全问题,以下是对懒汉方式创建单例模式的线程安全问题剖析。
懒汉模式代码:
public class DataSourceSingleton2 {
// 私有的构造方法
private DataSourceSingleton2() { // ②
}
// 私有属性
private static volatile DataSourceSingleton2 dataSource = null;// ①
// 公共的访问方法,得到单例对象
public static DataSourceSingleton2 getInstance() {
if(dataSource == null) { // 大致分流执行 ③
synchronized (DataSourceSingleton2.class) { // 排队执行
/** +1 */ if(dataSource == null) { // 到这里就只有一个可以被实例化了 ④
dataSource = new DataSourceSingleton2(); // ⑤
}
}
}
return dataSource;
}
以上代码是一个极为精简且十分合格的单例模式的代码,下面我们来解读一下:
(请大家根据以上代码注释中的标记来结合看以下解释)
首先,注意到 ① 处的volatile
关键字,它具备两个特征
解决了内存可见性问题,即就是:当一个线程修改了这个公共变量的值,新的值对于其他线程来说是可以立即得知的。
禁止了操作系统的指令重排序
这里主要是由于代码 ⑤ 处dataSource = new DataSourceSingleton2();
的这里的指令重排序问题。因为这个初始化操作并不是原子的,大体可分为如下三步:
dataSource
执行分配的内存空间JVM 允许再保证结果正确的前提下进行指令重排序优化。即如上3步可能的顺序为 1->2->3 或 1->3->2。如果顺序是 1->3->2,当3执行完,2还未执行时,另一个线程执行到代码③处,发现dataSource
不为null
,直接返回还未实例化的dataSource
并使用,此时这个内存空间所存储的内容就会被返回了,但是这里返回的空间还没有接收到我们对他的实例化,就会报错。
所以使用volatile
,就是为了保证线程间的可见性和防止指令重排序。
其次,在代码②处将构造函数声明为private
目的在于阻止在其它类中对此类生成新的实例。
最后,还值得一提的是,懒汉式代码需要使用双重检查锁,即 DCL (Double Check Lock)。那么为什么这样写呢?
有这样一种情况,线程1,2同时判断了第一次为空③,在加锁的地方阻塞了,如果没有第二次判空④,那么线程1 执行完毕后线程2 就会再次执行,这样就初始化了两次,就存在问题了。所以进入Synchronized
临界区以后,还要再做一次判空。因为两个线程同时访问的时候,线程1 构造完对象,线程2 也已经通过了最初的判空验证,不做第二次判断,线程2 还是会再次构造对象。
所谓 “饿汉方式” 就是说JVM在加载这个类时就马上创建此唯一的单例实例,不管你用不用,先创建了再说,如果一直没有被使用,使浪费了空间,典型的空间换时间,每次调用的时候,就不需要再判断,节省了运行时间。
为什么叫饿汉?
因为饿汉很饿,需要尽早初始化来喂饱自己。
饿汉式代码实现
public class DataSourceSingleton {
// 1.提供私有的构造方法
private DataSourceSingleton() {
}
// 2. 创建一个私有的属性对象,并直接实例化
private static DataSourceSingleton dataSource = new DataSourceSingleton();
// 3.提供公共的对外的单例对象
public static DataSourceSingleton getInstance() {
return dataSource;
}
}
饿汉模式特点:
public class DataSourceSingleton3 {
// 提供静态内部类,存在静态单例声明与初始化
private static class DataSourceSingletonHolder {
private static DataSourceSingleton3 dataSource = new DataSourceSingleton3();
}
// 私有静态单例对象
private DataSourceSingleton3(){
}
// 提供静态方法返回静态内部类中的单例对象
public static DataSourceSingleton3 getInstance() {
return DataSourceSingletonHolder.dataSource;
}
}
DataSourceSingletonHolder
是静态内部类,当外部类DataSourceSingleton3
被加载时并不会创建任何实例,只有当DataSourceSingleton3.getInstance()
被调用的时候,才会创建实例,这一切由 JVM 天然完成,所以既保证了线程安全,有实现了延迟加载。
public enum DataSourceSingleton4 {
INSTANCE;
public DataSourceSingleton4 getInstance() {
return INSTANCE;
}
}
使用时直接.INSTANCE.getInstance()
即可。
特点:
可以说枚举就是一个天生的单例,而且还可以自由序列化,反序列化后也是单例的。