四、单例模式概述
单例模式(Singleton),也叫单子模式,是一种常用的设计模式。在应用这个模式时,单例对对象的类必须保证只有一个实例处在。 许多时候系统只需要拥有一全局对象,这样有利于我们协调系统整体的行为。eg:在某个服务器读取连接数据库配置文件的程序中,该配置文件是由一个单例对象读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,不可能说每次读取该配置文件时,都要新new一个对象再去获取这些配置信息,显然是不合适的,所以单例模式简化了在复杂环境下的配置管理。
**综上所述,单例模式其实就是为了确保一个类只有一个实例,并为整个系统提供一个全全局访问点的一种方法。**
4.1 单例模式及其单线程环境下的经典实现
确保一个类只有一个实例,并为整个系统提供一个全局访问点 (向整个系统提供这个实例)。
单例模式是创建型模式。
单例模式的三要素:
- 私有的构造方法(防止多个用户new新实例)
- 指向自己实例的私有静态资源
- 以自己实例为返回值的静态公有方法
1、单例模式下的两种经典实现
在介绍单线程环境中单例模式的两种经典实现之前,我们有必要先解释一下 延迟加载 和 立即加载 两个概念。
- 延迟加载 : 等到真正使用的时候才去创建实例(运行时创建),不用时不去主动创建。
- 立即加载 : 在类初始化加载的时候就主动创建实例;
在单线程环境下,单例模式根据实例化对象的时机不同,有两种经典的实现方式:
- 懒汉模式(延迟加载-
非线程安全
):只有在真正使用的时候才会去实例化一个对象并交给自己的引用; - 饿汉模式(立即加载-
线程安全
):在单例类被初始化加载时,就实例化一个对象并交给自己的引用。
2、懒汉式单例:
新建一个ConfigManager类:
- ConfigManager:
package com.util; import java.io.IOException; import java.io.InputStream; import java.util.Properties; /** * @author 一宿君(CSDN : qq_52596258) * @date 2021-07-22 09:54:48 */ public class ConfigManager { //指向自己实例的私有静态资源 private static ConfigManager configManager = null; //操作读取文件的properties实例对象 private static Properties properties = null; /** * 私有的构造方法(防止多个用户new新实例) */ private ConfigManager(){ //使其new创建实例的时候,默认读取配置文件,且只读取一次 //先new一个Properties实例 properties = new Properties(); //使用IO流读取配置文件 InputStream is = this.getClass().getResourceAsStream("/database.properties"); //将读取文件加载到properties中 try { properties.load(is); } catch (IOException e) { e.printStackTrace(); } } /** * 只允许ConfigManager 被new一次, * 当再次被new的时候判断是否为空, * 不为空会直接返回上次new的实例 * * 懒汉式创建获取单例的方法 * @return */ //以自己实例为返回值的静态的公有方法,静态工厂方法 public static ConfigManager getInstance1(){ //只有在需要的时候才会去创建实例 if(configManager1 == null){ configManager1 = new ConfigManager(); } return configManager1; } /** * 通过Properties实例读取配置文件中的属性参数 */ public String getValue(String key){ return properties.getProperty(key); } }
- TestSingleton测试类:
package com.test; import com.thread.Thread1; import com.thread.Thread2; import com.util.ConfigManager; /** * @author 一宿君(CSDN : qq_52596258) * @date 2021-07-22 09:58:21 */ public class TestSingleton { public static void main(String[] args) { System.out.println(ConfigManager.getInstance1()); System.out.println(ConfigManager.getInstance1()); } }
我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。
3、饿汉式单例:
package com.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* @author 一宿君(CSDN : qq_52596258)
* @date 2021-07-22 09:54:48
*/
public class ConfigManager {
private static Properties properties = null;
/**
* 将构造方法私有化,防止多个用户new新实例
*/
private ConfigManager(){
//使其new创建实例的时候,默认读取配置文件,且只读取一次
//先new一个Properties实例
properties = new Properties();
//使用IO流读取配置文件
InputStream is = this.getClass().getResourceAsStream("/database.properties");
//将读取文件加载到properties中
try {
properties.load(is);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 2 饿汉模式 类初始化加载时创建实例
* @return
*/
// 指向自己实例的私有静态引用,主动创建
private static ConfigManager configManager2 = new ConfigManager();
//以自己实例为返回值的静态的公有方法,静态工厂方法
public synchronized static ConfigManager getInstance2(){
return configManager2;
}
/**
* 通过Properties实例读取配置文件中的属性参数
*/
public String getValue(String key){
return properties.getProperty(key);
}
}
- TestSingleton测试类:
我们知道,类加载的方式是按需加载,且加载一次。因此,在上述单例类被加载时,就会实例化一个对象(静态内部类)并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。
4、懒汉模式和饿汉模式的区别:
- 从速度和反应时间角度来讲,饿汉式(又称立即加载)要好一些;
- 从资源利用效率上说,懒汉式(只有用的时候才会去创建,又称延迟加载)要好一些。
5、单例模式的优点
- 在内存中只有一个对象,节省内存空间;
- 避免繁琐的创建销毁对象,可以提高性能;
- 避免对共享资源的多重利用,简化访问;
- 为整个系统提供一个全局访问点。
6、单例模式的应用场景
单例模式具有以上特点,并且形式上比较简单,所以是日常开发中用的比较多的一种设计模式,其核心在于为整个系统提供一个唯一的实例,其应用场景包括以下常见的两种:
- 有状态的工具类对象;
- 频繁访问数据库或文件(上述案例中的Properties读取数据库database.properties文件)的对象。
7、单例模式的注意事项
在使用单例模式时,我们必须使用单例类提供的公有工厂方法(getInstance())得到单例对象,而不应该使用反射来创建,否则将会实例化一个新对象。此外,在多线程环境下使用单例模式时,应该特别注意线程安全问题。
4.2 单例模式与多线程环境下的实现方式
在单线程环境下,无论是饿汉模式还是懒汉模式,他们都能正常工作。但是在多线程环境下,情形就发生了改变:
- 由于饿汉模式天生就是线程安全的,可以直接用于多线程而不会出现问题;
- 但懒汉模式本身是非线程安全的,因此在多线程环境下应用就会出现多个实例的情况。
为了能够更好的观察到单例模式的实现在多线程环境下是否是线程安全的,我们可以通过启动多个线程分别加载懒汉模式和饿汉模式下的创建单例对象的方法,然后分别打印获取到单例对象的hashCode值,判断多个线程得到的单例对象的hashCode值是否相同?如果相同,那么就说明是线程安全的;如果不相同,则说明是线程不安全的。
1、TestSingleton类测试上述的懒汉模式getInstance1()方法:
package com.test;
import com.thread.Thread1;
import com.thread.Thread2;
import com.util.ConfigManager;
/**
* @author 一宿君(CSDN : qq_52596258)
* @date 2021-07-22 09:58:21
*/
public class TestSingleton {
//使用内部静态类继承Thread线程类
public static class newThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + ConfigManager.getInstance1());
}
}
public static void main(String[] args) {
//创建一个线程数组
Thread[] threads = new Thread[5];
for(int i=0;i<threads.length;i++){
//创建5个线程
threads[i] = new newThread();
}
for(int i=0;i<threads.length;i++){
//启动5个线程
threads[i].start();
}
}
}
2、懒汉模式非线程安全的原因:
上面发生非线程安全的一个显著原因是,我们知道线程之间是竞争CPU执行资源,当多个线程之间的时间间隔非常短的时候,可以认为多个线程是同步进入if (ConfigManager == null) {…}
语句块的情形,所以就获取到了多个实例对象。当这种情形发生后,该单例类就会创建出多个实例,违背单例模式的初衷。因此,传统的懒汉式单例是非线程安全的。
3、如何解决懒汉模式的非线程安全
3.1、同步延迟加载 — synchronized方法
3.2、同步延迟加载 — synchronized块
:
我们是通过将synchronized修饰放在获取单实例的getInstance()方法上了,这样会有点影响效率,因为同步块的作用域有点大,而且同步锁的力度有点粗,所以我们可以通过同步代码块优化代码,并提升效率。
3.3、同步延迟加载 — 使用内部类实现延迟加载
再次测试TestSingleton类:
4、TestSingleton类测试上述的懒汉模式getInstance2()方法:
5、懒汉模式与饿汉模式实现单例的区别:
通过上述案例我们可以看出懒汉模式与饿汉模式实现单例的区别就在于:是否使用synchronized修饰getInstance()方法,如果使用就保证了对临界资源的同步互斥访问,也就保证了单例。
4.3、单例模式与双重检查(Double-Check idiom)的实现
- 基于懒汉模式的改进
/** * 双重检测Double-check-idiom */ private static ConfigManager configManager1 = null; //以自己实例为返回值的静态的公有方法,静态工厂方法 public static ConfigManager getInstance1(){ if(configManager1 == null){ synchronized(ConfigManager.class){ //只需在第一次创建单实例的时候才做同步互斥 if(configManager1 == null){ configManager1 = new ConfigManager(); } } } return configManager1; }
如上述代码所示,为了在保证单例的前提下提高运行效率,我们需要对 configManager1 进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同步获取锁了)。这种做法无疑是优秀的,但是我们必须注意一点:必须使用volatile关键字修饰单例引用。
volatile关键字的含义是:被其所修饰的变量的值不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存来实现,从而确保多个线程能正确的处理该变量。该关键字可能会屏蔽掉虚拟机中的一些代码优化,所以其运行效率可能不是很高,所以,一般情况下,并不建议使用双重加锁机制,酌情使用。
如果上述的实现没有使用 volatile 修饰 configManager1,那么将会导致JVM内部指令重排序,原理我们在此处先不做分析,一旦做了重排序程序的效率就受到了严重的影响。
4.4、单例模式 与 ThreadLocal
借助于 ThreadLocal,我们可以实现双重检查模式的变体。我们将临界资源线程局部化,具体到本例就是将双重检测的第一层检测条件 if (configManafer4 == null) 转换为 线程局部范围内的操作 。这里的 ThreadLocal 也只是用作标识而已,用来标识每个线程是否已访问过:如果访问过,则不再需要再走同步块,这样就提高了一定的效率。对应的代码清单如下:
/**
* 4、ThreadLocal-线程局部变量
*/
//ThreadLocal---线程局部变量
private static ThreadLocal<ConfigManager> threadLocal = new InheritableThreadLocal<ConfigManager>();
private static ConfigManager configManager4 = null;
private ConfigManager(){}
public static ConfigManager getInstance4(){
if(configManager4 == null){ //第一次判空:该线程是否是第一次访问
creatConfigManager();
}
}
public static void creatConfigManager(){
synchronized (ConfigManager.class){
if(configManager4 == null){ //第二次判空:该单例是否被创建
configManager4 = new ConfigManager(); //只执行一次
}
}
threadLocal.set(configManager4);//将单例引用放入当前线程的局部变量中
}
4.5、总结
上述我们首先介绍了单例模式的定义和结构,并给出了其在单线程和多线程环境下的几种经典实现。要清楚理解,传统的饿汉模式实现单例无论在单线程还是多线程环境下都是线程安全的,但是传统的懒汉式单例在多线程环境下是非线程安全的。为此,我们特别介绍了五种方式解决懒汉模式在多线程环境下创建线程安全的单例,包括:
- 使用synchronized方法实现懒汉式单例;
- 使用synchronized块实现懒汉式单例;
- 使用静态内部类实现懒汉式单例;
- 使用双重检查模式实现懒汉式单例;
- 使用ThreadLocal实现懒汉式单例;