我是靠谱客的博主 忧虑小海豚,最近开发中收集的这篇文章主要介绍设计模式之创建型模式---单例模式1.介绍2.应用场景3.实现总结,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

在这里插入图片描述

文章目录

  • 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倍还多, 这在性能要求严苛的应用中是不可忽视的.


总结

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

最后

以上就是忧虑小海豚为你收集整理的设计模式之创建型模式---单例模式1.介绍2.应用场景3.实现总结的全部内容,希望文章能够帮你解决设计模式之创建型模式---单例模式1.介绍2.应用场景3.实现总结所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(38)

评论列表共有 0 条评论

立即
投稿
返回
顶部