1. 引言

1.1 背景

当在应用程序中需要控制资源共享、进行配置管理和日志记录等操作时,一种常见的需求是希望通过一个全局访问点,让程序无论在哪个地方,只要能够访问到,就可以通过这个全局访问点,来获取相关实例信息。为满足这种需求,我们可以采用单例模式(Singleton Pattern)。单例模式确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

具体来说,单例模式通常会提供一个静态方法(例如getInstance()),这个方法返回类的唯一实例。由于这个方法是静态的,因此它可以在不创建类实例的情况下被调用。这意味着任何代码只要能够访问到该类,就可以通过调用这个静态方法来获取单例实例。

1.2 目的

本文将详细介绍单例模式的基本概念、实现步骤。通过本篇文章,你将能够理解单例模式的工作原理,并学会如何在实际项目中有效地利用它。

2. 何为单例模式?

讲个趣味性点的例子,单例模式就像是一个动漫世界里的主角光环,无论剧情如何发展,主角永远只有一个,而且每个人都知道他是故事的核心。这样,无论故事如何展开,大家都能找到同一个人来推动剧情。

2.1 单例模式的优缺点

优点

确保单一实例:避免重复创建实例,节省资源。
全局访问点:方便全局访问,简化调用。
延迟初始化:按需创建实例,提高性能。

缺点

难以扩展:单例类通常难以扩展,因为构造函数是私有的。
潜在的性能问题:在高并发环境下,某些实现方式可能会有性能问题。
测试困难:单例模式可能会导致测试困难,因为它是全局状态。

2.2 单例模式的使用场景

根据单例模式的特点,它的使用场景可以分为如下几个:

  • 比如说在资源共享的情况下,可以将配置文件数据、日志文件放在一个文件中,这些配置数据或日志文件由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这样可以简化在复杂环境下的配置管理。
  • 在控制资源的情况下,比如说线程池中,多线程的线程池的设计一般采用单例模式,方便对池中的线程进行控制。

3. 单例模式的实现模式

单例模式的实现通常包括三个要素:

  1. 私有构造方法,将类的构造函数设为私有,这样外部就无法通过 new 关键字来创建实例。
  2. 私有静态引用指向自己实例,在类内部创建一个静态的实例变量,用于保存唯一的实例。
  3. 以自己实例为返回值的公有静态方法,提供一个静态方法,让外部可以通过这个方法获取到唯一的实例。

3.1 饿汉式单例模式

对于饿汉式单例模式,单例实例在类装载时就构建,线程安全,因为在类加载的同时已经创建好一个静态对象,调用时反应速度快。缺点也很明显,资源效率不高,只要执行该类的其他静态方法或者加载了该类,这个实例仍然会初始化。

/**    
 * 饿汉单例模式:在还没有实例化的时候就初始化
 */
public class Hungry {    
  	//1. 开始时就创建实例
	private static final Hungry instance=new Hungry();
	
	// 2. 私有化的构造方法
	private void hungry() {  
	}
	
	public static Hungry getInstance() { 
		// 返回单例名
		return instance;  		
	}
}

3.2 懒汉式单例模式

对于懒汉式单例模式,单例实例在第一次被使用时构建,延迟初始化,相对资源利用率高。缺点是当多个线程同时访问就可能同时创建多个实例,而这多个实例不是同一个对象,虽然后面创建的实例会覆盖先创建的实例,但还是会存在拿到不同对象的情况。

/**
 * 懒汉单例模式:没有用就不初始化,要用时,才初始化
 */
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关键字对实例初始化前后进行加锁。缺点是第一次加载时反应不快,多线程使用不必要的同步开销大。

/**
 * 双重检查锁定的懒汉模式
 */
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 静态内部类

静态内部类,资源利用率高,单例实例在第一次使用时构建,延迟初始化。缺点是第一次加载时反应不够快。

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 枚举单例模式

利用枚举实现单例,在还没有实例化的时候就初始化,简洁且线程安全。

public enum Singleton {
    INSTANCE;

    // 添加需要的属性和方法
    public void someMethod() {
        // 方法实现
    }
}

4. 单例模式的具体实现流程

4.1 如何使用单例模式管理配置文件数据?

  1. 首先,基于SpringBoot项目,假设有配置文件application.properties:
db.host=localhost
db.port=3306
  1. 接着,使用单例模式,管理配置文件
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);
    } 
}
  1. 通过ConfigurationManager.getInstance() 获取单例实例
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 如何使用单例模式实现线程池?

在多线程环境中,线程池通常采用单例模式来确保全局只有一个线程池实例,从而方便对池中的线程进行控制和管理。

  1. 首先依旧是构建一个单例类,用于管理线程池
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();
    }
}
  1. 通过ThreadPoolManager.getInstance() 获取单例实例
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只会有一个实例,并且这个实例会被所有需要它的地方共享。例如

import org.springframework.stereotype.Component;

@Component
public class SingletonBean {
   // Bean的实现
}

依旧是举个示例:使用Spring Bean的单例模式获取配置文件中,数据库的基本信息。

  1. 首先,配置文件信息还是假设application.properties :
db.host=localhost
db.port=3306
  1. 创建一个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;
}
  1. 在Spring Boot应用的启动类绑定配置属性
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);
    }
}
  1. 最后,只需要注入DbConfig Bean,就可以使用这些配置属性
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);
    }
}

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部