一、什么是单例模式
单例模式属于创建型设计模式,是Java种最简单的设计模式之一。
这种模式要求一个类仅有一个实例,并且提供了一个全局的访问点。在程序中多次使用同一个对象且作用相同时,所有需要调用的地方就会共享这个访问点(对象),从而防止因频繁创建对象而损耗系统性能。
二、优缺点
优点:
- 全局唯一实例:通过单例模式,可以确保一个类只有一个实例对象存在,全局范围内可以方便地访问该实例。
- 节省资源:由于只有一个实例存在,单例模式可以节省系统资源(如内存、CPU等),避免多次创建和销毁对象的开销。
- 避免对共享资源的竞争:在多线程环境中,单例模式可以避免对共享资源的竞争问题,确保数据一致性和线程安全性。
缺点:
- 不支持多态:单例模式一般只能创建一个固定类型的对象实例,不支持多态的灵活性。
- 降低了代码的灵活性:对于使用了单例模式的代码,如果需要改变实例化策略或使用其他类型的实例,可能需要修改代码和重新设计。
- 隐藏了依赖关系:单例模式可能隐藏了对其他组件或对象的依赖关系,使得代码的结构不够清晰,增加了代码的理解和维护的难度。
三、应用场景
3.1 生活场景
- 一个班级只有一个班主任。
- windows桌面上的回收站:我们打开一个回收站,当我们再次试图打开一个新的时,Windows并不会为你弹出一个新窗口,也就是说整个系统运行过程中,系统只维护一个回收站的实例。
- 网站计数器:一般也采用单例模式实现,如果你存在多个计数器,每一个用户访问都刷新的计数器,这样的话你的实际数量是难以同步的。
3.2 Java场景
Bean定义的默认作用域:在Spring中,默认情况下,所有的Bean都是单例的,也就是说 Spring 容器中只会创建一个特定类型的Bean实例。
Spring容器(ApplicationContext):Spring容器本身也是一个使用了单例模式的对象。无论是基于XML配置的ClassPathXmlApplicationContext,还是基于注解的AnnotationConfigApplicationContext,它们都是单例的,只会生成一个容器实例。
Spring AOP中的切面(Aspect):在Spring AOP中,切面是用来定义横切关注点(如日志、事务等)的类。默认情况下,Spring会将切面定义为单例,以确保在整个应用程序中只有一个切面实例,以避免创建过多的代理对象。
四、创建单例
注意看代码中的注释。
4.0 代码结构
4.1 饿汉式(2种)
/**
* 饿汉:在类刚一初始化的时候就立即把单例对象创建出来,下面两种都是饿汉模式的实现
*/
public class Singleton {
private Singleton() {}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private static Singleton instance1=null;
static {
instance1=new Singleton();
}
public Singleton getInstance1(){
return instance1;
}
}
4.2 懒汉式 (4种)
/**
* 懒汉:懒加载,就是在需要的时候才回去创建对象
*/
public class Singleton {
private Singleton() {
}
/**
* 1.单例模式【线程不安全,不推荐】
* 因为没有加锁synchronized,严格意义上不算单例。
* @return
*/
private static Singleton instance1;
public static Singleton getInstance() {
if (instance1 == null) {//这里是不安全的,可能得到两个不同的实例
instance1 = new Singleton();
}
return instance1;
}
/**
* 2.线程安全但效率低【不推荐】
* 99%的情况下不需要同步
* @return
*/
private static Singleton instance2;
public static synchronized Singleton getInstance1() {
if (instance2 == null) {
instance2 = new Singleton();
}
return instance2;
}
/**
* 3.单例模式,线程不安全【不推荐】
* 虽然加了锁,但是等到第一个线程执行完instance2=new Singleton();跳出锁时
* 令一个线程恰好刚判断完instance2为null,此时又会加载另一个实例
*/
private static Singleton instance3;
public static Singleton getInstance2() {
if (instance3 == null) {
synchronized (Singleton.class) {//不安全
instance3 = new Singleton();
}
}
return instance3;
}
/**
* 4.双重校验锁:延迟加载+线程安全【推荐】
*/
private static volatile Singleton instance4;
public static Singleton getInstance4() {
if (instance4 == null) {
synchronized (Singleton.class) {
if (instance4 == null) {
instance4 = new Singleton();
}
}
}
return instance4;
}
}
4.3 静态内部类
/**
* 静态内部类【推荐】
* 这种方式跟饿汉式方式采用的机制类似,但又有不同。
* 两者都是采用了类装载的机制来保证初始化实例时只有一个线程。
* 不同的地方:
* 在饿汉式方式是只要Singleton类被装载就会实例化,
* 内部类是在需要实例化时,调用getInstance方法,才会装载SingletonHolder类
* 优点:避免了线程不安全,延迟加载,效率高。
*/
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
4.4 枚举
/**
* 枚举实现
* 这种方式还没有被广泛采用,但是这种是实现单例的最佳方式。
* 线程安全,自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化
*/
public enum Singleton {
INSTANCE;
public void whateverMethod(){
System.out.println("单例模式实现的最佳方式");
}
}
五、总结
- 单例最常见的写法:懒汉式、饿汉式。
- 懒汉式:懒加载,在调用的时候才会实例化对象,推荐使用双重校验锁的方式。
- 饿汉式:类加载的时候就已经实例化好对象了,不会存在并发安全和性能问题。
- 在开发中,如果内存要求高,就用懒汉式,不高则用饿汉式。
- 最优雅的方式是使用枚举,代码极简,没有线程安全问题,又能防止反射和序列化时破坏单例。
END:更多设计模式的介绍,推荐移步至👉 23种设计模式学习导航(Java完整版)