设计模式之创建型模式---单例模式
创始人
2024-04-21 14:35:28

在这里插入图片描述

文章目录

  • 1.介绍
  • 2.应用场景
  • 3.实现
    • 3.1 结构
    • 3.2 类图
    • 3.3 代码示例
      • 3.3.1 饿汉式
      • 3.3.2 懒汉式
      • 3.3.3 双重检验锁
      • 3.3.3 静态内部类实现单例
      • 3.3.4 枚举类实现单例
  • 总结

1.介绍

单例模式(singleton) 是指某个类中能生成一个实例,该类提供了一个全局访问点,提供一个唯一的实例给外部调用,这样做的目的是为了节省资源,减少垃圾回收的消耗,保证数据的一致性,对某些类要求只能创建一个实例(对象)。

2.应用场景

单例模式的应用场景有数据库的连接池,应用程序中的对话框,系统中的缓存,多线程中的线程池等

3.实现

3.1 结构

单例模式主要有两个角色,一个是实现了单例的类,另一个是使用单例的类。

单例类: 单例类就是实现单例的类,也就是这个类中提供了一个方法给外部用于获取这个类的实例,这个实例在这个方法中会做唯一性判断,即这个类的对象如果创建过了,就直接使用,否则再创建。
访问类: 使用单例的类,也就是客户端类。通俗说就是我们要使用这个单例类对象的地方

3.2 类图

在这里插入图片描述注:类图不了解的小伙伴要自己去了解哦,这里不做扩展了

3.3 代码示例

3.3.1 饿汉式

饿汉式:指的是类加载的时候就进行初始化

public class Singleton {//声明一个私有的本类对象private static Singleton INSTANCE = new Singleton();//将构造函数私有化,让外部调用者只能通过我们提供的方法创建对象private Singleton(){}//饿汉式public static Singleton getInstance(){return INSTANCE;}public void testFun(){System.out.println("测试单例模式");}
}public class Client {public static void main(String[] args) {Singleton instance1 = Singleton.getInstance();Singleton instance2 = Singleton.getInstance();System.out.println("instance1: " + instance1  +  " ,instance2: " + instance2);instance1.testFun();}
}

优点: 线程安全,因为JVM在加载这个类的时候就会进行初始化,包括对静态变量的初始化。
缺点: 空间浪费,饿汉式是使用空间换时间,不判断直接创建,假设创建了后不使用这个对象,就造成了空间浪费。如果单例类的体积比较大的话,空间的浪费也是不容忽视的。

运行结果

在这里插入图片描述

3.3.2 懒汉式

懒汉式:指的是在使用实例的时候再进行初始化,但是这种方式在多线程情况下使用会有问题,即这种方式是线程不安全的

public class Singleton {//声明一个私有的本类对象private static Singleton INSTANCE =  null;//将构造函数私有化,让外部调用者只能通过我们提供的方法创建对象private Singleton(){}public static Singleton getInstance(){if(INSTANCE == null){INSTANCE = new Singleton();}return INSTANCE;}public void testFun(){System.out.println("测试单例模式");}
}

优点:节省空间,使用的时候才会创建实例对象。
缺点:线程不安全。

3.3.3 双重检验锁

双重检验锁是对懒汉式的改进,使其可以在多线程的场景中使用

public class Singleton {//声明一个私有的本类对象private volatile static Singleton INSTANCE =  null;//将构造函数私有化,让外部调用者只能通过我们提供的方法创建对象private Singleton(){}public static Singleton getInstance(){//先判断实例是否存在if(INSTANCE == null){//加锁创建实例synchronized (Singleton.class){//再次判断,因为可能会出现某个线程拿到锁后,还没来得及执行初始化就释放了锁,//这时假如其他线程拿到了锁又执行到了这里的话会创建一个实例,这样就会出现多个实例if(INSTANCE == null){INSTANCE = new Singleton();}}}return INSTANCE;}public void testFun(){System.out.println("测试单例模式");}
}

这里我们可以看到有几个改进,首先是声明本类的实例时加了一个voltile 关键字;
private volatile static Singleton INSTANCE = null;
因为jvm创建对象的过程不是原子的,步骤如下。
① 在堆内存中, 为新的实例开辟空间;
② 初始化构造器, 对实例中的成员进行初始化;
③ 把这个实例的引用 (也就是这里的instance) 指向①中空间的起始地址.
如果1-3步骤不是原子性的,那么在创建对象的过程中,jvm可能会做指令优化,也就是对1-3的顺序做重排序,比如2在1的前面。这样就会导致创建出来的对象是不完整的,是无法使用的。而且还不好定位这个问题,所以volatile关键字的作用就是可以禁止jvm做指令重排序

3.3.3 静态内部类实现单例

静态内部类的方式是线程安全的,并且是懒加载的方式,即使用的时候才会去创建类的对象,是懒汉式的变形

注意: jvm 加载类的时候:步骤为: 加载 -> 验证-> 准备 -> 解析 -> 初始化,并且JVM在加载外部类的过程中,不会加载静态内部类,只有内部类的属性/方法被调用的时候才会被加载,并初始化静态属性

public class Singleton {//将构造函数私有化,让外部调用者只能通过我们提供的方法创建对象private Singleton(){}public static Singleton getInstance(){return SingletonHolder.INSTANCE;}private static class SingletonHolder{private static Singleton INSTANCE = new Singleton();}public void testFun(){System.out.println("测试单例模式");}
}

静态内部类实现单例
优点:不加锁,线程安全,用到的时候才会加载,并发性能高,推荐使用

3.3.4 枚举类实现单例

JDK5 开始,提供了枚举,枚举其实是一个语法糖,让我们可以少写一些代码,JVM编译的时候会帮我们添加很多额外的信息,枚举类可以在JVM层面保证线程安全

enum Singleton{INSTANCE;//枚举类使用单例可以直接使用Singleton.INSTANCE 获取单例使用public void testFun(){System.out.println("枚举类实现单例");}
}//枚举单例使用方法
public class Client {public static void main(String[] args) {Singleton instance1 = Singleton.INSTANCE;Singleton instance2 = Singleton.INSTANCE;System.out.println("instance1: " + instance1  +  " ,instance2: " + instance2);instance1.testFun();}
}

优点: 不需要考虑序列化的问题:枚举序列化是由JVM保证的, 每一个枚举类型和枚举变量在JVM中都是唯一的, 在枚举类型的序列化和反序列化上Java做了特殊的规定: 在序列化时Java仅仅是将枚举对象的name属性输出到结果中, 反序列化时只是通过java.lang.Enum#valueOf()方法来根据名字查找枚举对象 —— 编译器不允许对这种序列化机制进行定制、并且禁用了writeObject、readObject、readObjectNoData、writeReplace、readResolve等方法, 从而保证了枚举实例的唯一性;

不需要考虑反射的问题: 在通过反射方法

java.lang.reflect.Constructor#newInstance()//创建枚举实例时, JDK源码对调用者的类型进行了判断:// 判断调用者clazz的类型是不是Modifier.ENUM(枚举修饰符), 如果是就抛出参数异常:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)throw new IllegalArgumentException("Cannot reflectively create enum objects");

所以, 我们是不能通过反射创建枚举实例的, 也就是说创建枚举实例只有编译器能够做到.保证了安全
缺点: 所有的属性都必须在创建时指定, 也就意味着不能延迟加载; 并且使用枚举时占用的内存比静态变量的2倍还多, 这在性能要求严苛的应用中是不可忽视的.


总结

本节主要介绍了单例的几种创建方式,推荐使用静态内部类的方式,也可以使用双重检验锁的方式。在开发中也是这两种方式使用得最多。读者还有其他好的方式的话可以评论区讨论

相关内容

热门资讯

猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...