掌握单例模式,让你的代码更优雅!
掌握单例模式,让你的代码更优雅!
单例模式是Java中最简单的设计模式之一,属于创建型模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。通过饿汉式、懒汉式等实现方法,单例模式能够有效提升代码效率和资源利用率。掌握单例模式,不仅能让你的代码更加简洁高效,还能避免不必要的资源浪费,使你的编程之路如虎添翼。
单例模式的实现方式
单例模式主要有两种实现方式:饿汉式和懒汉式。
饿汉式单例模式
饿汉式单例模式是指在类加载时就创建了该类的实例,因此被称为“饿汉”模式。在这种模式下,无论是否需要使用该实例,都会提前创建出来。这样可以保证在后续的使用中,直接返回该实例,没有额外的线程安全问题。
//饿汉式构建单例类
class Singleton {
//1.静态私有数据成员,产生唯一对象
private static Singleton single = new Singleton();
//2.私有化构造方法
private Singleton() { }
//3.取得唯一实例对象的公共静态成员方法
public static Singleton getInstance() {
return single;
}
}
饿汉式的缺点是可能会浪费内存空间,因为实例在类加载时就被创建了。
懒汉式单例模式
懒汉式单例模式是指在需要使用该实例时才进行创建,因此被称为“懒汉”模式。在这种模式下,首次调用获取实例的方法时,才会进行实例化。
懒汉式的优点是节省了内存空间,只有在需要使用时才会创建实例。然而,懒汉式的实现需要考虑线程安全问题,需要在获取实例的方法中进行同步操作,以避免多线程下出现重复创建实例的情况。
//懒汉式构建单例类
class Singleton{
//1.静态私有数据成员,没有产生唯—对象
private static Singleton single= null;
private Singleton() { } //2.私有化构造方法
//3.取得唯一实例对象的公共静态成员方法,如果没有唯一实例化对象则产生该对象
public static Singleton getInstance(){
if (single==null)
single= new Singleton();
return single;
}
}
为了保证线程安全,可以使用双重校验锁(Double-Checked Locking)的方式:
public class SingletonLazy {
//定义一个私有构造方法
private SingletonLazy(){}
//创建该类对象
private volatile static SingletonLazy instance;
//对外提供静态方法获取该对象 , 加双重锁
public static SingletonLazy getInstance(){
if(instance==null){
synchronized (instance.getClass()){
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
}
此外,还可以使用静态内部类的方式,利用JVM类加载机制保证线程安全:
public class SingletonLazy {
//私有构造方法
private SingletonLazy() {}
//定义静态类
private static class SingletonHolder {
//静态方法
private static final SingletonLazy INSTANCE = new SingletonLazy();
}
//对外提供静态方法获取该对象
public static SingletonLazy getInstance() {
return SingletonHolder.INSTANCE;
}
}
单例模式的优势与局限性
单例模式具有以下优势:
- 资源优化:单例模式通过避免重复创建对象来减少内存消耗,特别是在频繁实例化会导致性能下降的场景下。
- 统一访问控制:它为唯一实例提供了受控的全局访问方式,并简化了对共享资源的管理。
- 防止资源多重占用:例如,在写文件操作中,单例模式可以避免多个线程同时写入同一文件的问题。
- 系统设置与资源共享:适用于需要集中管理和优化资源访问的场景,如数据表映射处理。
然而,单例模式也存在一些局限性:
- 扩展困难:缺乏抽象层导致单例类难以扩展,限制了灵活性。
- 职责过重:单例类可能承担过多责任,违背单一职责原则。
- 滥用风险:过度使用可能导致问题,比如数据库连接池溢出或对象状态丢失。
单例模式的应用场景
单例模式在实际开发中有着广泛的应用场景:
- 配置管理器:提供全局唯一的配置信息访问点。
- 日志记录器:集中管理日志输出,方便追踪程序运行状态。
- 数据库连接池:统一管理数据库连接,提高资源利用率。
- 缓存管理器:高效管理缓存数据,提升访问速度。
单例模式 vs 原型模式
在Spring框架中,单例模式是默认的bean作用域。在IOC容器中,每个ID只对应一个bean实例,该实例在第一次创建后会被缓存,以后每次请求该bean,都会直接返回第一次创建的实例。这样可以确保在一个Spring IoC容器的上下文中,每个bean都是唯一的。
而原型模式在每次请求它的时候都会创建一个新的bean实例,即使同一个ID,由于作用域是原型,所以返回的也是不同的实例。
@Configuration
public class AppConfig {
@Bean
@Scope("prototype")
public StudentService studentService() {
return new StudentService();
}
}
在单例模式下(即没有取消注释 @Scope("prototype")
行), bean1
和 bean2
实际上指向同一个对象,它们的哈希码也因此相同。在原型模式下(即取消注释 @Scope("prototype")
行),每次通过 context.getBean(MyBean.class);
获取的都将是不同的对象,因此, bean1
和 bean2
的哈希码将会不同,表明它们是不同的对象。
原型模式的应用场景可以大致的参考一下以下案例:
- 状态不可共享bean:假设我们有一个
ShoppingCart
类,这个类包含了当前用户的购物车信息:用户ID,商品列表等。由于每个用户的购物车都是不同的,这就需要为每个用户创建一个新的购物车实例。这样每一个实例就可以拥有自己的状态(用户ID,商品列表),并且这些状态之间不会互相干扰。在这种情况下,为ShoppingCart
设置成原型模式就很合适 - 重要资源对象:对于一些创建和销毁成本较高的对象,如果每次需要时都去创建和销毁,那么系统的开销会非常大。例如,在一些大型软件应用中,可能会为了提供服务而创建许多对数据库的连接(数据库连接的建立和销毁成本高),这个时候可以将数据库连接设置为原型模式,这样每次需要数据库连接时,都从Spring容器获取一个新的实例,替代新建一个数据库连接。
- 在运行时动态确定,创建哪一类的实例:在有些情况下,你可能需要在运行时动态决定创建哪类对象。例如,你有多种排序算法类,这些算法类都实现自同一个接口。你可以在运行时,基于不同的输入或者条件,选择使用哪种排序算法,这时,可以将每种排序算法类设为原型Bean,这样就可以动态地从Spring中获取特定的排序算法实例。
在实际开发中,选择哪种模式取决于具体需求。如果Bean是有状态的,或者需要每次都得到一个全新的对象实例,那么原型模式可能是一个好选择。如果Bean是无状态的,或者多个组件共享一个Bean实例没有问题,那么单例模式可能更有优势,因为它可以减少对象创建的开销。