🍅 作者简介:哪吒,CSDN2021博客之星亚军🏆、新星计划导师✌、博客专家💪
🍅 哪吒多年工作总结:Java学习路线总结,搬砖工逆袭Java架构师
🍅 技术交流:定期更新Java硬核干货,不定期送书活动
🍅 关注公众号【哪吒编程】,回复 1024 ,获取《10万字208道Java经典面试题总结(附答案)》2024修订版pdf,背题更方便,一文在手,面试我有
目录
- 一、简述一下生产场景:
- 初始化FTP代码样例:
- 二、Java中一个类中的static对象,在JVM中,会存在多个吗?
- 三、要保证唯一,是否可以通过Singleton单例模式解决?
- 先对比一下Singleton和static:
- 1、相似之处
- 2、关键区别
- 3、使用Singleton单例模式改造上述代码
- 四、具体说一下Singleton是什么?
- 1、Singleton是指只能被实例化一次的类
- 2、实现方式
- 3、Signleton通常用于表示无状态的对象
- 五、Singleton模式中,static变量需要设置成final吗,为什么?
- 1、使用final
- 2、使用final的具体场景
- 3、不需要final的场景
- 4、使用final的具体场景,数据库连接池
- 六、Singleton模式什么场景中需要使用序列化?
- 1、持久化
- 2、缓存系统的持久化
- 3、分布式系统
- 七、Singleton模式中使用序列化的潜在问题
- 八、单元素的枚举类型往往是实现singleton的最佳选择?还解决了序列化问题?
- 1、实现起来非常简单
- 2、线程安全
- 3、序列化问题
- 4、防止反射攻击
- 5、延迟加载
- 6、可以实现接口
最近一段时间,一直在解决多线程情况下,通过FTP上传文件,报错的问题。
一、简述一下生产场景:
- 核心需求:凌晨定时上传文件到FTP服务器
- 最开始的代码是单线程实现的,但是速度较慢,需要优化一下。
- 根据某字段进行拆分,采用多线程的方式,速度是提升了,但同样也产生了问题(在
channelSftp.cd(directory);
提示java.io.IOException: Pipe closed
) - 经过问题的定位分析,结论如下:多线程情况下,其它线程将ftp连接关闭了,导致cd时,channelSftp为无效链接。
- 最初的解决方案是:在多线程初始化ftp时,将下面“初始化FTP代码样例:”中的if (sftpConnectionPool == null) {干掉,每次都重新初始化一遍。问题就解决了,上线、搞定,下班,睡觉!
这里想探讨一个问题:
现场半夜出现故障,客户半夜联系技术人员,技术人员因静音没接到电话,导致现场程序宕机10小时(早上上班才看到并解决),客户投诉,导致技术人员无赔偿被裁,这合理吗?
初始化FTP代码样例:
public static SftpConnectionPool sftpConnectionPool = null;
//初始化
public static boolean init() {
try{
//if (sftpConnectionPool == null) {
String username = "nezha";
String password = "123456";
String host = "127.0.0.1";
int port = 22;
sftpConnectionPool = new SftpConnectionPool(100, host, port, username, password);
//}
}catch(Exception e){
logger.error("|#初始化SFTP|#异常:" , e);
return false;
}
return true;
}
但,在代码review的时候,技术专家问了我一句,“每次重新初始化static的sftpConnectionPool变量变量,内存中会不会有多个FTP连接池?它们还是一个吗?”
给我干懵逼了!我犹豫了一下,“是的”,但内心是充满不确定的!
作为技术链最顶端的男人,我必须搞清楚这个问题。
二、Java中一个类中的static对象,在JVM中,会存在多个吗?
先说答案,在Java中,一个类中的static对象在JVM中只会存在一个实例。
static表示类级别的成员,而不是实例级别的。
也就是说,static变量或方法是属于整个类的,而不是属于某个特定的实例。
当一个类被加载时,JVM会为该类分配内存,并初始化static变量,这些变量在类的所有实例之间共享,并且在内存中只有一个副本。
不论你创建多少个类的实例,static变量在内存中始终只有一个副本,所有实例共享一个static变量 。
static变量是在类第一次被加载的时候被创建和初始化。即便在多线程环境下,static对象也不会因为线程的并发创建多个实例。
得出结论,上述代码,不管实例化多少次,Java内存中只会有一个FTP连接池。
自信些,伙计,我说的没错。
三、要保证唯一,是否可以通过Singleton单例模式解决?
很多人,开发多年,依旧感觉Signleton和static好像是一样的,比如我。
先对比一下Singleton和static:
1、相似之处
- 全局访问点:两者都提供了一种全局访问的方式。
- 单一实例:两者都可以确保只有一个实例存在。
2、关键区别
(1)初始化时机
- singleton:可以实现延迟初始化(懒加载)。
- static:在类加载时就会初始化。
(2)继承和多态
- singleton:可以实现继承,支持多态。
- static:静态成员不能被继承或重写。
(3)接口实现
- singleton:可以实现接口。
- static:静态类不能实现接口。
(4)状态管理
- singleton:可以管理实例状态,更灵活。
- static:所有的静态成员都共享同一个状态。
(5)序列化
- singleton:可以实现序列化,但需要特殊处理以维护单例性质。
- static:静态字段不会被序列化。
(6)资源管理
- singleton:可以实现生命周期管理,例如在应用程序关闭时释放资源。
- static:无法确保资源的及时释放。
虽然Singleton和static看起来相似,但Singleton提供了更多的灵活性和控制。它允许延迟初始化、更好的资源管理、更容易的测试和更灵活的设计。然而,这种灵活性是以稍微复杂的实现为代价的。
选择使用static还是Singleton应该基于具体的需求:
- 如果只需要一组简单的工具方法,static可能就足够了。
- 如果需要更复杂的状态管理、更好的测试性或更灵活的设计,Singleton可能是更好的选择。
3、使用Singleton单例模式改造上述代码
- 使用了私有的构造函数,防止外部直接实例化。
- 使用volatile关键字修饰instance变量,确保其在多线程环境下的可见性。
- 使用双重检查锁定模式来确保线程安全,同时避免了不必要的同步开销。
- 将原来的init()方法的逻辑移到了getInstance()方法中,实现了懒加载。
- 异常处理方面,我们将异常记录到日志中,然后抛出一个RuntimeException。这样可以确保在初始化失败时,调用者能够及时知道并处理这个问题。
private static volatile SftpConnectionPool instance = null;
private SftpConnectionPoolSingleton() {
// 私有构造函数,防止直接实例化
}
public static SftpConnectionPool getInstance() {
if (instance == null) {
synchronized (SftpConnectionPoolSingleton.class) {
if (instance == null) {
try {
String username = "nezha";
String password = "123456";
String host = "127.0.0.1";
int port = "22";
instance = new SftpConnectionPool(100, host, port, username, password);
} catch (Exception e) {
logger.error("|#初始化SFTP|#异常:", e);
throw new RuntimeException("SFTP连接池初始化失败", e);
}
}
}
}
return instance;
}
这样,就可以在整个应用程序中使用同一个SftpConnectionPool实例了。
需要注意的是,第一次调用getInstance()时会初始化连接池,可能会花费一些时间,后续的调用将直接返回已创建的实例。
四、具体说一下Singleton是什么?
1、Singleton是指只能被实例化一次的类
Singleton通常指的就是单例模式,确保某个类在整个应用程序生命周期中只能被实例化一次。
无论你在应用程序的任何地方创建多少个这个类的实例,系统始终返回同一个Singleton实例。
通过这种方式,保证了全局的唯一性和一致性。
2、实现方式
实现Singleton的方式有很多种(核心理念都是一样的),这里介绍最常用的一种:
- 将类的构造函数设为私有或受保护,防止直接通过new关键字创建实例;
- 提供一个公共的静态方法来获取实例。在这个方法或属性内部,判断实例是否已经创建过,如果没有创建则实例化一个,如果已创建则返回现有实例。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
// 私有构造函数,防止外部实例化
}
public static Singleton getInstance() {
return instance;
}
}
3、Signleton通常用于表示无状态的对象
无状态的对象是指对象的属性值不随时间和使用而改变,或者即使改变了也不会影响应用程序的逻辑。例如,一个工具类会提供一些通用方法,这些方法不依赖于对象的属性值变化。
典型的无状态类可以是一个数据库连接管理器或者一个日志记录器。
Singleton模式的核心在于控制实例的唯一性,而无状态对象因为不需要存储特定状态,所以可以通过Singleton模式方便地实现共享和重用,减少系统的资源开销。
五、Singleton模式中,static变量需要设置成final吗,为什么?
1、使用final
将static变量设置为final,意味着一旦instance被初始化后,就不能再被重新赋值。这样可以确保Singleton实例的不可变性,防止在代码中任何地方意外地修改这个唯一实例。
由于final变量必须在声明时或在构造器中初始化,在类加载时立即创建并初始化final修饰的Singleton实例。
使用final的,通常更简单且线程安全。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
// 私有构造函数,防止外部实例化
}
public static Singleton getInstance() {
return instance;
}
}
2、使用final的具体场景
(1)配置管理器
当你的应用程序需要一个全局的配置对象,而且这个配置在启动时就确定且不会改变。
INSTANCE 是 final 的,因为配置在应用启动时就加载完毕,不需要改变。
public class ConfigManager {
private static final ConfigManager INSTANCE = new ConfigManager();
private final Map<String, String> config;
private ConfigManager() {
config = loadConfig(); // 从文件或数据库加载配置
}
public static ConfigManager getInstance() {
return INSTANCE;
}
public String getConfig(String key) {
return config.get(key);
}
private Map<String, String> loadConfig() {
// 实现配置加载逻辑
}
}
(2)日志管理器
日志管理器通常在应用启动时就初始化,之后不会改变。
public class LogManager {
private static final LogManager INSTANCE = new LogManager();
private final Logger logger;
private LogManager() {
logger = Logger.getLogger("ApplicationLogger");
}
public static LogManager getInstance() {
return INSTANCE;
}
public void log(String message) {
logger.info(message);
}
}
3、不需要final的场景
如果需要懒加载或在运行时根据条件选择不同的实例时,不需要final。
在这种实现中,你需要确保线程安全性,通常会用同步、双重检查锁定等技术来确保实例的唯一性。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
4、使用final的具体场景,数据库连接池
在某些情况下,你可能需要根据运行时的条件来初始化连接池,或者需要在运行时更换连接池实现。
instance 不是 final 的,因为我们需要延迟初始化,并可能根据不同的数据库配置创建不同的实例。
public class DatabaseConnectionPool {
private static DatabaseConnectionPool instance;
private ConnectionPool pool;
private DatabaseConnectionPool() {}
public static synchronized DatabaseConnectionPool getInstance() {
if (instance == null) {
instance = new DatabaseConnectionPool();
}
return instance;
}
public void initializePool(String dbUrl, String username, String password) {
// 根据传入的参数初始化连接池
pool = new SomeConnectionPoolImplementation(dbUrl, username, password);
}
public Connection getConnection() {
return pool.getConnection();
}
}
六、Singleton模式什么场景中需要使用序列化?
1、持久化
你正在开发一个桌面应用程序,需要在应用关闭时保存当前的应用状态,并在下次启动时恢复。
为什么需要序列化:
- 应用状态需要持久化到磁盘。
- 在应用重新启动时,需要从磁盘读取并恢复状态。
在下面代码示例中,AppStateManager 不仅可以被序列化,还提供了 saveState 和 loadState 方法来实现状态的保存和加载。
import java.io.*;
public class AppStateManager implements Serializable {
private static final long serialVersionUID = 1L;
private static volatile AppStateManager instance;
private Map<String, Object> stateData;
private AppStateManager() {
stateData = new HashMap<>();
}
public static AppStateManager getInstance() {
if (instance == null) {
synchronized (AppStateManager.class) {
if (instance == null) {
instance = new AppStateManager();
}
}
}
return instance;
}
public void setState(String key, Object value) {
stateData.put(key, value);
}
public Object getState(String key) {
return stateData.get(key);
}
public void saveState(String filename) throws IOException {
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
out.writeObject(this);
}
}
public static void loadState(String filename) throws IOException, ClassNotFoundException {
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
instance = (AppStateManager) in.readObject();
}
}
protected Object readResolve() {
return instance;
}
}
2、缓存系统的持久化
你正在实现一个缓存系统,该系统需要在服务器重启或计划内维护时将缓存内容持久化,以避免冷启动带来的性能问题。
为什么需要序列化:
- 缓存内容需要快速地保存到磁盘和从磁盘恢复。
- 缓存管理器是一个单例,需要在序列化和反序列化过程中保持一致性。
import java.io.*;
import java.util.concurrent.ConcurrentHashMap;
public class CacheManager implements Serializable {
private static final long serialVersionUID = 1L;
private static volatile CacheManager instance;
private ConcurrentHashMap<String, Object> cache;
private CacheManager() {
cache = new ConcurrentHashMap<>();
}
public static CacheManager getInstance() {
if (instance == null) {
synchronized (CacheManager.class) {
if (instance == null) {
instance = new CacheManager();
}
}
}
return instance;
}
public void put(String key, Object value) {
cache.put(key, value);
}
public Object get(String key) {
return cache.get(key);
}
public void persistCache(String filename) throws IOException {
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
out.writeObject(this);
}
}
public static void loadCache(String filename) throws IOException, ClassNotFoundException {
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
instance = (CacheManager) in.readObject();
}
}
protected Object readResolve() {
return instance;
}
}
3、分布式系统
比如在分布式系统中,有一个配置管理器,它使用Singleton模式来确保配置在整个应用程序中是唯一的。比如数据库连接池。为了提高性能,配置管理器可能会被多个JVM实例共享。
这时候就需要通过网络传输Singleton对象,或者在多个进程之间共享对象状态,这时候需要将Singleton对象序列化为字节流进行传输。
import java.io.Serializable;
public class ConfigManager implements Serializable {
private static final long serialVersionUID = 1L;
private static volatile ConfigManager instance;
private Map<String, String> configData;
private ConfigManager() {
configData = new HashMap<>();
}
public static ConfigManager getInstance() {
if (instance == null) {
synchronized (ConfigManager.class) {
if (instance == null) {
instance = new ConfigManager();
}
}
}
return instance;
}
public void setConfig(String key, String value) {
configData.put(key, value);
}
public String getConfig(String key) {
return configData.get(key);
}
// 确保反序列化时返回同一个实例
protected Object readResolve() {
return getInstance();
}
}
在这些场景中,序列化单例模式的关键点是:
- 实现 Serializable 接口。
- 提供一个 readResolve() 方法以确保反序列化时返回单例实例。
- 小心处理静态字段,因为它们通常不会被序列化。
- 考虑线程安全问题,特别是在反序列化过程中。
使用序列化的单例模式时,需要格外注意保持单例的一致性和线程安全性。同时,也要考虑序列化可能带来的安全风险,确保敏感数据得到适当的保护。
七、Singleton模式中使用序列化的潜在问题
如果直接序列化一个Singleton对象,然后反序列化,反序列化的结果是一个新的对象实例,而不是原来的Singleton实例。这就破坏了Singleton的唯一性。
为了确保在序列化和反序列化过程中不会破坏Singleton模式,通常需要在Singleton类中实现readResolve方法。这个方法会在反序列化过程中被调用,用于确保返回的是当前的Singleton实例,而不是一个新的实例。
import java.io.ObjectStreamException;
import java.io.Serializable;
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
// 私有构造函数,防止外部实例化
}
public static Singleton getInstance() {
return INSTANCE;
}
// 确保反序列化时返回同一个实例
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
八、单元素的枚举类型往往是实现singleton的最佳选择?还解决了序列化问题?
1、实现起来非常简单
public enum SingletonEnum {
INSTANCE;
// 可以添加方法
public void doSomething() {
// 实现逻辑
}
}
通过SingletonEnum.INSTANCE.doSomething();
就可以直接用了。
2、线程安全
枚举的实例创建是由JVM在类加载时完成的,天然就是线程安全的。
3、序列化问题
Java确保枚举类型的序列化和反序列化是绝对安全的。JVM会保证反序列化时返回的是同一个实例,不会创建新的实例。
4、防止反射攻击
通过反射创建枚举实例会抛出异常,这进一步保证了单例的唯一性。
5、延迟加载
虽然枚举常量在类加载时就被初始化,但你可以通过内部类来实现延迟加载:
public class SingletonEnum {
private SingletonEnum() {}
private enum SingletonHolder {
INSTANCE;
private final SingletonEnum singleton;
SingletonHolder() {
singleton = new SingletonEnum();
}
}
public static SingletonEnum getInstance() {
return SingletonHolder.INSTANCE.singleton;
}
}
6、可以实现接口
枚举可以实现接口,这提供了额外的灵活性:
public interface MySingleton {
void doSomething();
}
public enum SingletonEnum implements MySingleton {
INSTANCE;
@Override
public void doSomething() {
// 实现逻辑
}
}
👉 GPT功能:
- GPT-4o知识问答:支持1000+token上下文记忆功能
- 最强代码大模型Code Copilot:代码自动补全、代码优化建议、代码重构等
- DALL-E AI绘画:AI绘画 + 剪辑 = 自媒体新时代
- 私信哪吒,直接使用GPT-4o
🏆文章收录于:100天精通Java从入门到就业
哪吒数年工作总结之结晶。
🏆哪吒多年工作总结:Java学习路线总结,搬砖工逆袭Java架构师。
华为OD机试 2023B卷题库疯狂收录中,刷题点这里
刷的越多,抽中的概率越大,每一题都有详细的答题思路、详细的代码注释、样例测试,发现新题目,随时更新,全天CSDN在线答疑。