文章目录
- 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倍还多, 这在性能要求严苛的应用中是不可忽视的.
总结
本节主要介绍了单例的几种创建方式,推荐使用静态内部类的方式,也可以使用双重检验锁的方式。在开发中也是这两种方式使用得最多。读者还有其他好的方式的话可以评论区讨论