Android 单例模式必知必会

2023-05-16

目录

一、概念

1.1 单例类

1.2 优缺点

1.2.1 优点

1.2.2 缺点

二、创建单例模式的方法

2.1 饿汉式

2.2 懒汉式

2.2.1 懒汉式(非线程安全)

2.2.2 懒汉式(线程安全)

2.3 双重检验锁

2.4 静态内部类

2.5 枚举

小结

三、扩展

3.1 防止反序列化

3.2 volatile 关键字


一、概念

        单例模式是运用最广泛的设计模式之一,在应用这个模式时,单例模式的类必须保证只有一个实例存在。多用于整个程序只需要有一个实例,通常很消耗资源的类,比如线程池,缓存,网络请求,IO操作,访问数据库等。由于类比较耗资源,所以没必要让它构造多个实例,这种就是单例模式比较好的使用场景。

1.1 单例类

单例模式(Singleton Pattern):一个类有且仅有一个实例,并且自行实例化向整个系统提供,也称为单例类。

单例模式有三个要点:

  • 1.某个类只能有一个实例。

  • 2.必须自行创建这个实例。

  • 3.必须给所有其他对象提供这一实例。

具体实现角度来说就是以下几点:

  • 1.单例模式的类只提供私有的构造函数

  • 2.通过一个静态方法或者枚举返回单例类对象

  • 3.确保单例类有且只有一个静态私有对象,尤其是在多线程环境下。

  • 4.提供了一个静态的公有的函数用于创建或获取它本身的静态私有对象。

  • 5.确保单例类对象在反序列化时不会重新构建对象。

在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。

1.2 优缺点

1.2.1 优点

  • 1.单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。

  • 2.由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。

  • 3.允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。

1.2.2 缺点

  • 1.由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。

  • 2.单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。

  • 3.现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

二、创建单例模式的方法

2.1 饿汉式

        强调饿,那么在创建对象实例的时候就比较着急,饿了嘛,于是在装载类的时候就创建对象实例。

        这种方法非常简单,因为单例的实例被声明成 static 和 final 变量,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

public class SingletonHungry {
    //类加载时就初始化
    private static final SingletonHungry singleton = new SingletonHungry();
    private SingletonHungry(){}
    public static SingletonHungry getInstance(){
        return singleton;
    }
}

        缺点是它不是一种懒加载模式,即使客户端没有调用 getInstance()方法,单例会在加载类后一开始就被初始化。

        饿汉式的创建方式在一些场景中将无法使用:如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

2.2 懒汉式

        强调懒,那么在创建对象实例的时候就不着急,什么时候用什么时候创建。所以在装载对象的时候不创建对象实例。

2.2.1 懒汉式(非线程安全)

public class SingletonLazy {
    private static SingletonLazy singletonLazy;
    private SingletonLazy(){}
    public static SingletonLazy getInstance(){
        if (singletonLazy == null) {
            singletonLazy = new SingletonLazy();
        }
        return singletonLazy;
    }
}

        这段代使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。那么怎么解决?最简单的方法是给 getInstance() 方法加个同步锁(synchronized)。

2.2.2 懒汉式(线程安全)

public class SingletonLazy {
    private static SingletonLazy singletonLazy;
    private SingletonLazy(){}
    public static synchronized SingletonLazy getInstance(){
        if (singletonLazy == null) {
            singletonLazy = new SingletonLazy();
        }
        return singletonLazy;
    }
}

        上面通过添加 synchronized 关键字,使得getInstance()是一个同步方法,保证多线程情况下单例对象的唯一性。

        虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁

2.3 双重检验锁

        双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,也是网上使用毕竟频繁的一种方式。

        为什么叫双重检查锁?因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,避免不必要的同步。如果在同步块内不进行二次检验的话就会生成多个实例,避免生成多个实例

public class SingletonDCL {
    private static SingletonDCL singleton;
    private SingletonDCL(){}
    public static SingletonDCL getInstance(){
        if (singleton == null) {
            synchronized (SingletonDCL.class){
                if (singleton == null) {
                    singleton = new SingletonDCL();
                }
            }
        }
        return singleton;
    }
}

        这段代码看起来很完美有着双重检查,但很可惜,它是有问题。主要在于 singleton = new SingletonDCL()。事实上在 JVM 中这句话大概做了下面 3 件事情:

  • 1.给 singleton 分配内存。

  • 2.调用 SingletonDCL 的构造函数来初始化成员变量。

  • 3.将singleton对象指向分配的内存空间(执行完这步 singleton 就为非 null)。

        但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第2步和第3步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、但 2 未执行之前,被线程二抢占了,这时 singleton 已经是非 null 了(但却没有初始化),所以线程二会直接返回 singleton(第2步未执行),然后使用,然后顺理成章地报错。

        我们只需要将 singleton 变量声明成 volatile 就可以了。

public class SingletonDCL {
    private volatile static SingletonDCL singleton;//变量声明成volatile
    private SingletonDCL(){}
    public static SingletonDCL getInstance(){
        if (singleton == null) {
            synchronized (SingletonDCL.class){
                if (singleton == null) {
                    singleton = new SingletonDCL();
                }
            }
        }
        return singleton;
    }
}

        使用 volatile 的主要原因是其有一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。

        当然 volatile 变量还有一个规则:对一个变量的写操作先行发生于后面对这个变量的读操作(这里的"后面"是时间上的先后顺序)。

2.4 静态内部类

public class SingletonNested {
    //静态内部类
    private static class SingletonHolder{
        private static final SingletonNested singleton = new SingletonNested();
    }
    private SingletonNested(){}
    public static SingletonNested getInstance(){
        return SingletonHolder.singleton;
    }
}

        使用JVM本身机制保证了线程安全问题。由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化SingletonNested,第一次调用getInstance()时将加载内部类SingletonHolder,在该内部类中定义了一个static类型的变量singleton,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。 s

        由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的,同时读取实例的时候不会进行同步,没有性能缺陷,也不依赖 JDK 版本。

2.5 枚举

public enum SingletonEnum {
    SINGLETON;
    public void doSomeThing() {
    }
}

        我们可以通过SingletonEnum.SINGLETON来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象

小结

单例模式不管用那种方式实现,核心思想都相同:

  • 1.构造函数私有化,通过一次静态方法获取一个唯一实例

  • 2.线程安全

使用场景:

  • 1.需要频繁的进行创建和销毁的对象。

  • 2.创建对象时耗时过多或耗费资源过多,但又经常用到的对象。

  • 3.工具类对象。

  • 4.频繁访问数据库或文件的对象。

        一般情况下直接使用饿汉式就好了,当然推荐使用文中DCL方式静态内部类的方式来创建单例模式。如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。当然,枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。

三、扩展

3.1 防止反序列化

        上文使用枚举可以防止反序列化导致重新创建新的对象。那么其他几种实现单例模式的方式怎么方式防止反序列化导致重新创建新的对象?那就是反序列化。可以参考序列化一文。

        反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的readResolve()函数,这个函数可以让开发人员控制对象的反序列化。

public class SingletonDCL implements Serializable {
    private volatile static SingletonDCL singleton;//变量声明成volatile
    ...
    private Object readResolve() throws ObjectStreamException {
        return singleton;
    }
}

        在readResolve方法中将单例对象返回,而不是重新生成一个新对象。

3.2 volatile 关键字

        Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

作用

  • 1.线程可见性

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

  • 2.指令重排序

没加之前,指令是并发执行的,第一个线程执行到一半另一个线程可能开始执行了。加了volatile关键字后,不同线程是按照顺序一步一步执行的。例如上面2.3 双重检验锁

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android 单例模式必知必会 的相关文章

  • IBMX3650M4安装win2008Server操作指南

    IBMX3650M4安装win2008Server操作指南 由于IBM服务器是IBM原有的Linux系统 xff0c 所以需要在此硬件上安装Win 2008 Server系统 xff08 以下简称win8 xff09 xff0c 中间遇到了
  • windows远程桌面连接到服务器黑屏问题

    任务管理器使用组合键 Ctrl 43 Alt 43 End xff0c 于是打开了远程电脑的任务管理器 xff0c 取消 xff0c 即可返回桌面正常显示
  • p2p数据分析

    大数据在许多国家具有战略意义 xff0c 其原因不仅在少数巨头公司的应用 xff0c 而是大 中 小 微企业乃至个人 xff0c 都能利用大数据来创造新的价值 而互联网本身就是一个大数据的金矿 xff0c 利用它创造价值 xff0c 是时代
  • 关于The GPG keys listed for the "***" repository are already installed but they are not corret

    这个问题是在自己制作的repo源码库的时候出现的 xff0c 从字面上理解 xff0c 是 为 源码仓库出示的GPG密钥已经安装 xff0c 但不正确 首先 xff0c GPG密钥存在的目的是处于安全和规范考虑 xff0c RedHat在发
  • __stack_chk_fail问题分析

    一 问题 进程收到SIGABRT 信号异常退出 xff0c 异常调用栈显示 stack chk fail Build fingerprint 39 Pico A7H10 PICOA7H10 10 5 5 0 smartcm 16769120
  • 十套精美个人博客网站模板

    文件资源 点击下载 展示在下方 xff0c 点击你想下载的文件 xff0c 然后点击普通下载就能下载了 紫色的图片博客个人页面模板 红色的微博社交平台HTML模板 响应式生活博客设计网站HTML5模板 程序员个人博客模板 响应式的互联网IT
  • 高性能无锁环形队列 Disruptor

    Disruptor 环形队列 JLog 秒级百G级日志搜集 传输 存储解决方案 高性能无锁队列 Disruptor 高性能队列 Disruptor 使用教程 高性能队列Disruptor框架的详细说明与实战使用 SpringBoot 并发框
  • ubuntu20.04更换阿里的软件源

    新安装的ubuntu20 04的软件源是使用的国外的源 xff0c 因此在使用apt安装软件时速度并不怎么快 xff0c 建议大家更换为国内的源 xff0c 这样在使用apt安装软件时速度会有明显的提升的 ubuntu20 04 apt的配
  • mpi运行窗口无反应或者闪退

    原因有三 1 xff0c 服务未启动 2 xff0c 系统防火墙拦截
  • Newtonsoft.Json使用,C# Json文件读取,写入

    用学校作为例子 xff0c 有学校名称 xff0c 学校下面有班级 xff0c 班级有名字 xff0c 班级下面有学生 xff0c 这里面有数组 xff0c 有字段 using System using System Collections
  • Motrix全能下载工具使用

    Motrix是一款界面简约 功能丰富 专业可靠的全能下载工具 先下载 CSDN下载 Motrix zip下载 官方下载地址 Motrix 打开Motrix xff0c 将种子文件放到这里 开始下载
  • ActiveMQ-JMS(五):ObjectMessage的安全问题

    安全问题 按照apache官网的说明 xff0c 为了避免收到恶意代码 xff0c 引入了安全机制 xff0c 只允许指定的包里的对象能够被传输 原文如下 xff1a ObjectMessage objects depend on Java
  • 剑指offer 03

    span class token keyword class span span class token class name Solution span span class token punctuation span span cla
  • 「得印度者,得天下」聊聊你不知道的印度在线视频江湖

    印度 xff0c 一个神奇古老的国度 千百年来 xff0c 恒河水鉴证了古印度王朝的兴衰更迭 xff0c 壮丽的历史文化 xff0c 和印度文明缘起缘灭的生死轮回 时光飞转 xff0c 来到公元 2018年 恒河水波澜不惊一切如昨 xff0
  • Trinity简介(1)--用于无参考基因组的转录组de novo组装

    一 Trinity简介 Trinity xff0c 是由 the Broad Institute 开发的转录组de novo组装软件 xff0c 由三个独立的软件模块组成 xff1a Inchworm Chrysalis和Butterfly
  • Trinity进行转录组组装(2))

    1 Trinity进行转录组组装 Trinity进行转录组组装的典型命令如下 opt biosoft trinityrnaseq r20131110 Trinity pl seqType fq JM 50G left sample1 1 c
  • python的两种退出方式

    os exit vs sys exit 转自 xff1a http www cnblogs com gaott archive 2013 04 12 3016355 html 概述 python的程序有两种退出方式 xff1a os exi
  • R语言数据类型转化

    R语言数据类型转化 转自 xff1a http www wangluqing com 2014 09 10 r share34 有时候 xff0c 对于一些问题 xff0c 需要进行数据类型之间的转换 R提供了基本类型转换函数以解决数据类型
  • ubuntu20.04安装中文输入法

    虽然搜狗的官网已经宣传说已经支持2004 2010 xff0c 但是支持的并不完美 xff0c 闪退 xff0c 打不出字各种问题不断 xff0c 所以本文带领大家安装几款能够正常使用的中文输入法 但是正在我要发这篇博客的时候 xff0c
  • R语言做柱状图

    R语言做柱状图 转自 xff1a http www phperz com article 16 0102 180120 html 条形图代表在与条成比例的变量的值的长度矩形条数据 R使用函数barplot 来创建柱状图 R能够绘制柱状图垂直

随机推荐

  • R语言 PCA(主成分分析)

    R语言 PCA 转自 xff1a http www cnblogs com longzhongren p 4300593 html 1 关键点 综述 xff1a 主成分分析 因子分析 典型相关分析 xff0c 三种方法的共同点主要是用来对数
  • 使用Pandas对数据进行筛选和排序

    使用Pandas对数据进行筛选和排序 转自 xff1a http bluewhale cc 2016 08 06 use pandas filter and sort html 筛选和排序是Excel中使用频率最多的功能 xff0c 通过这
  • linux 下安装blat软件

    linux 下安装blat软件 blat是一款很经典的比对工具 xff0c 与blast相比 xff0c 具有速度快 共线性输出比对结果等优点 但是 xff0c blat源码包里面的README文件写得很不清楚 xff0c 这里 xff0c
  • 基于统计的压缩算法:游程编码

    原网址 xff1a http www cnblogs com xudong bupt p 3761417 html 基于统计的压缩算法 xff1a 游程编码 1 游程编码概念 游程编码又称 运行长度编码 或 行程编码 xff0c 是一种统计
  • BWT (Burrows–Wheeler_transform)数据转换算法

    原网址 xff1a https blog csdn net luanzheng 365 article details 78575429 BWT Burrows Wheeler transform 数据转换算法 1 什么是BWT 压缩技术主
  • pip使用豆瓣的镜像源

    抄自 xff1a https www cnblogs com ZhangRuoXu p 6370107 html pip使用豆瓣的镜像源 豆瓣镜像地址 xff1a https pypi douban com simple 虽然用easy i
  • PyVCF

    抄自 xff1a https www cnblogs com nkwy2012 p 9204088 html vcf文件的全称是variant call file xff0c 即突变识别文件 xff0c 它是基因组工作流程中产生的一种文件
  • 【Kotlin 初学者】扩展-享受编程

    作者简介 xff1a CSDN博客专家 华为云 云享专家认证 系列专栏 xff1a Kotlin 初学者 学习交流 xff1a 三人行必有我师焉 xff1b 择其善者而从之 xff0c 其不善者而改之 目录 一 介绍 二 扩展函数 2 1
  • 【Kotlin 初学者】函数式编程

    作者简介 xff1a CSDN博客专家 华为云 云享专家认证 系列专栏 xff1a Kotlin 初学者 五星好评 xff1a 左侧点一下 网页端 xff0c 移动端 xff1a https bbs csdn net topics 6039
  • centos8.5 更新失败

    今天使用yum makecache的时候出现了Error Failed to download metadata for repo 39 base 39 Cannot download repomd xml Cannot download
  • 【Kotlin 初学者】Java和Kotlin互操作

    作者简介 xff1a CSDN博客专家 华为云 云享专家认证 系列专栏 xff1a Kotlin 初学者 五星好评 xff1a 左侧点一下 网页端 xff0c 移动端 xff1a https bbs csdn net topics 6039
  • Kotlin 基础知识汇总(知识与实践相结合)

    2个月的时间总算把 Kotlin 的基础知识写完了 xff0c 下面咱们看看具体内容 xff1a 学习 Kotlin 的必要性 Kotlin 初学者 为什么要学Kotlin Kotlin 初学者 打牢基础的重要性 运行环境 Kotlin 初
  • HashMap的产生与原理

    一 HashMap的诞生 1 1 数组 数组 xff1a 一片物理上连续的大小确定的储存空间 好处 xff1a 根据下标快速的查找和修改里面的内容 缺点 xff1a 大小确定 xff0c 无法修改 添加新的元素或者删除元素比较麻烦 数组的静
  • Android 数据存储(一)-文件存储

    目录 一 数据存储概念 二 应用程序专属文件存储 2 1 访问持久文件 2 2 将数据存储到文件 2 3 从文件中读取数据 2 4 查看文件列表 2 5 删除文件 三 缓存文件 cache目录下 3 1 创建缓存文件 3 2 删除文件 四
  • 回顾2021,展望2022 | 年终总结

    你付出多少努力 xff0c 就必有多少收获 一 回顾 2021 2021 年输出109篇文章 xff0c 收获 xff1a 博客专家认证 Android领域新星创作者认证 博客之星Top50 同时也在问答模块解决了部分小伙伴的问题 xff0
  • Android 数据存储(二)-SP VS DataStore VS MMKV

    一 SharedPreferences 不同于文件的存储方式 xff0c 如果要保存的键值集合相对较小 xff0c 则应使用SharedReferences API SharedReferences对象指向一个包含键值对的文件 xff0c
  • Jetpack DataStore 你总要了解一下吧?

    目录 一 DataStore 介绍 Preferences DataStore 和 Proto DataStore 二 Preferences DataStore 2 1 添加依赖 2 2 使用 Preferences DataStore
  • Android 数据存储(四)-Room

    目录 一 概述 1 1 描述 1 2 主要部件 二 创建 Room 2 1 添加依赖项 2 2 创建数据实体 2 2 1 设置 tableName or name 属性 2 2 2 设置主键 2 2 3 忽略字段 2 3 创建数据访问对象
  • Android 抛弃IMEI改用ANDROID_ID

    介绍 之前一直使用IMEI作为唯一标识符 xff0c 缺点就是需要权限 xff1a Android 10以前 xff0c 需要READ PHONE STATE权限 Android 10限制 xff0c 需要READ PRIVILEGED P
  • Android 单例模式必知必会

    目录 一 概念 1 1 单例类 1 2 优缺点 1 2 1 优点 1 2 2 缺点 二 创建单例模式的方法 2 1 饿汉式 2 2 懒汉式 2 2 1 懒汉式 非线程安全 2 2 2 懒汉式 线程安全 2 3 双重检验锁 2 4 静态内部类