1. 引言
1.1 背景
当在应用程序中需要控制资源共享、进行配置管理和日志记录等操作时,一种常见的需求是希望通过一个全局访问点,让程序无论在哪个地方,只要能够访问到,就可以通过这个全局访问点,来获取相关实例信息。为满足这种需求,我们可以采用单例模式(Singleton Pattern)。单例模式确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
具体来说,单例模式通常会提供一个静态方法(例如getInstance()),这个方法返回类的唯一实例。由于这个方法是静态的,因此它可以在不创建类实例的情况下被调用。这意味着任何代码只要能够访问到该类,就可以通过调用这个静态方法来获取单例实例。
1.2 目的
本文将详细介绍单例模式的基本概念、实现步骤。通过本篇文章,你将能够理解单例模式的工作原理,并学会如何在实际项目中有效地利用它。
2. 何为单例模式?
讲个趣味性点的例子,单例模式就像是一个动漫世界里的主角光环,无论剧情如何发展,主角永远只有一个,而且每个人都知道他是故事的核心。这样,无论故事如何展开,大家都能找到同一个人来推动剧情。
2.1 单例模式的优缺点
优点
确保单一实例 :避免重复创建实例,节省资源。
全局访问点 :方便全局访问,简化调用。
延迟初始化:按需创建实例,提高性能。
缺点
难以扩展 :单例类通常难以扩展,因为构造函数是私有的。
潜在的性能问题 :在高并发环境下,某些实现方式可能会有性能问题。
测试困难:单例模式可能会导致测试困难,因为它是全局状态。
2.2 单例模式的使用场景
根据单例模式的特点,它的使用场景可以分为如下几个:
- 比如说在资源共享的情况下,可以将配置文件数据、日志文件放在一个文件中,这些配置数据或日志文件由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这样可以简化在复杂环境下的配置管理。
- 在控制资源的情况下,比如说线程池中,多线程的线程池的设计一般采用单例模式,方便对池中的线程进行控制。
3. 单例模式的实现模式
单例模式的实现通常包括三个要素:
- 私有构造方法,将类的构造函数设为私有,这样外部就无法通过 new 关键字来创建实例。
- 私有静态引用指向自己实例,在类内部创建一个静态的实例变量,用于保存唯一的实例。
- 以自己实例为返回值的公有静态方法,提供一个静态方法,让外部可以通过这个方法获取到唯一的实例。
3.1 饿汉式单例模式
对于饿汉式单例模式,单例实例在类装载时就构建,线程安全,因为在类加载的同时已经创建好一个静态对象,调用时反应速度快。缺点也很明显,资源效率不高,只要执行该类的其他静态方法或者加载了该类,这个实例仍然会初始化。
java
/**
* 饿汉单例模式:在还没有实例化的时候就初始化
*/
public class Hungry {
//1. 开始时就创建实例
private static final Hungry instance=new Hungry();
// 2. 私有化的构造方法
private void hungry() {
}
public static Hungry getInstance() {
// 返回单例名
return instance;
}
}
3.2 懒汉式单例模式
对于懒汉式单例模式,单例实例在第一次被使用时构建,延迟初始化,相对资源利用率高。缺点是当多个线程同时访问就可能同时创建多个实例,而这多个实例不是同一个对象,虽然后面创建的实例会覆盖先创建的实例,但还是会存在拿到不同对象的情况。
java
/**
* 懒汉单例模式:没有用就不初始化,要用时,才初始化
*/
public class Slacker {
// 1. 静态属性,存储单例
private static Slacker instance = null;
// 2. 私有的构造器,限制外部不能访问
private void slacker() {
}
// 3. 静态方法,获取单例
public static Slacker getInstance() {
if (instance == null) {
// 初始化
instance = new Slacker();
}
// 4. 返回单例名
return instance;
}
}
3.3 双重检测懒汉式单例模式
双重检测懒汉式单例模式,就是为了解决懒汉式单例模式的缺点的,使用了synchronized关键字对实例初始化前后进行加锁。缺点是第一次加载时反应不快,多线程使用不必要的同步开销大。
java
/**
* 双重检查锁定的懒汉模式
*/
public class LockUp {
// 1. 静态属性,存储单例
private static volatile LockUp instance = null;
// 2. 私有的构造器,限制外部不能访问
private LockUp() {
}
// 3. 静态方法,获取单例
public static LockUp getInstance() {
if (instance == null) {
// 加锁保证一次运行一个
synchronized (LockUp.class) {
if (instance == null) {
instance = new LockUp();
}
}
}
return instance;
}
}
3.4 静态内部类
静态内部类,资源利用率高,单例实例在第一次使用时构建,延迟初始化。缺点是第一次加载时反应不够快。
java
public class Singleton {
private Singleton() {
// 初始化代码
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
3.5 枚举单例模式
利用枚举实现单例,在还没有实例化的时候就初始化,简洁且线程安全。
java
public enum Singleton {
INSTANCE;
// 添加需要的属性和方法
public void someMethod() {
// 方法实现
}
}
4. 单例模式的具体实现流程
4.1 如何使用单例模式管理配置文件数据?
- 首先,基于SpringBoot项目,假设有配置文件application.properties:
properties
db.host=localhost
db.port=3306
- 接着,使用单例模式,管理配置文件
java
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
public class ConfigurationManager {
// 1. 静态实例,存储单例
private static ConfigurationManager instance;
// 2. 配置属性,用于加载配置文件
private Properties properties;
// 3. 私有的构造器,限制外部不能访问
private ConfigurationManager() {
properties = new Properties();
try {
// 读取配置文件
FileInputStream fileInputStream = new FileInputStream("application.properties");
properties.load(fileInputStream);
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 4. 静态方法,获取单例
public static synchronized ConfigurationManager getInstance() {
if (instance == null) {
instance = new ConfigurationManager();
}
return instance;
}
// 6. 公共方法,提供获取配置文件属性的功能
public String getProperty(String key) {
return properties.getProperty(key);
}
}
- 通过ConfigurationManager.getInstance() 获取单例实例
java
public static void main(String[] args) {
// 获取单例实例
ConfigurationManager configManager = ConfigurationManager.getInstance();
// 获取配置信息
String dbHost = configManager.getProperty("db.host");
String dbPort = configManager.getProperty("db.port");
System.out.println(dbHost + ":" + dbPort);
}
4.2 如何使用单例模式实现线程池?
在多线程环境中,线程池通常采用单例模式来确保全局只有一个线程池实例,从而方便对池中的线程进行控制和管理。
- 首先依旧是构建一个单例类,用于管理线程池
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolManager {
// 1. 静态实例,存储单例
private static ThreadPoolManager instance;
// 2. 设置线程池
private ExecutorService executorService;
// 3. 使用私有的构造器,限制外部不能访问
private ThreadPoolManager() {
// 创建一个固定大小的线程池
executorService = Executors.newFixedThreadPool(10);
}
// 4. 静态方法,获取单例
public static synchronized ThreadPoolManager getInstance() {
if (instance == null) {
instance = new ThreadPoolManager();
}
return instance;
}
// 5. 公共方法,提供提交任务到线程池的功能
public void submitTask(Runnable task) {
executorService.submit(task);
}
// 6. 公共方法,提供关闭线程池的功能
public void shutdown() {
executorService.shutdown();
}
}
- 通过ThreadPoolManager.getInstance() 获取单例实例
java
public static void main(String[] args) {
// 获取单例线程池实例
ThreadPoolManager threadPoolManager = ThreadPoolManager.getInstance();
// 提交任务到线程池,假设有10个任务
for (int i = 0; i < 10; i++) {
final int taskNumber = i;
threadPoolManager.submitTask(() -> {
System.out.println("当前编号为" + taskNumber + "的线程名称是:" + Thread.currentThread().getName());
});
}
// 关闭线程池
threadPoolManager.shutdown();
}
4.3 Spring Bean的单例模式管理配置文件数据
如果学过Spring的小伙伴,应该清楚,Spring框架中,默认情况下管理的Bean是单例的,这也意味着Spring容器在创建和管理Bean时,每个Bean只会有一个实例,并且这个实例会被所有需要它的地方共享。例如
java
import org.springframework.stereotype.Component;
@Component
public class SingletonBean {
// Bean的实现
}
依旧是举个示例:使用Spring Bean的单例模式获取配置文件中,数据库的基本信息。
- 首先,配置文件信息还是假设application.properties :
properties
db.host=localhost
db.port=3306
- 创建一个Java类绑定配置属性
java
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Data;
@Component
@ConfigurationProperties(prefix = "db")
public class DbConfig {
private String host;
private String port;
}
- 在Spring Boot应用的启动类绑定配置属性
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(DbConfig.class)
public class MyAppApplication {
public static void main(String[] args) {
SpringApplication.run(MyAppApplication.class, args);
}
}
- 最后,只需要注入DbConfig Bean,就可以使用这些配置属性
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SingletonService {
@Autowired
private DbConfig dbConfig;
public void getProperty() {
System.out.println(dbHost + ":" + dbPort);
}
}