Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【设计模式系列(二)】彻底搞懂单例模式

【设计模式系列(二)】彻底搞懂单例模式

作者头像
你好戴先生
发布于 2020-09-02 01:52:52
发布于 2020-09-02 01:52:52
54800
代码可运行
举报
文章被收录于专栏:戴言泛滥戴言泛滥
运行总次数:0
代码可运行

文章中涉及到的代码,可到这里来拿:https://gitee.com/daijiyong/DesignPattern

概念:单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点

关键点是:单例类中有一个静态的变量,私有化构造方法,提供唯一全局调用方法,并选择一个时机进行初始化

属于创建型模式

它主要是为了解决:一个全局使用的类频繁地创建与销毁所造成的资源开销

单例模式比较简单,最复杂的地方在于如何保证多线程、序列化等情况下

仍然保证单例实例的唯一性

使用场景:

  • 配置文件,如ServletContext、ServletConfig、ApplicationContext、数据库连接池
  • 要求生产唯一序列号
  • WEB 中的计数器,不用每次刷新都在数据库中同步一次,可以用单例先缓存起来

1. 饿汉式

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();
    private HungrySingleton() {
    }
    public static HungrySingleton getInstance() {
        return instance;
    }
}

在类加载的时候实例就初始化了

所以基于 classloader 机制很好的避免了多线程情况下的同步问题

还有一种写法是这样的

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class HungrySingleton {
    private static HungrySingleton instance;
    static {
        instance = new HungrySingleton();
    }
    private HungrySingleton() {
    }
    public static HungrySingleton getInstance() {
        return instance;
    }
}

类加载顺序:

先静态后动态、先上后下、先属性后方法

所以这种写法也能满足在类加载的时候就能初始化的要求

懒汉式的优点是:执行效率高,性能高(没有加锁)

缺点:不过不是明确需要初始化这个实例,存在内存浪费的情况

2. 懒汉式单例

为了解决饿汉式中存在的内存浪费的情况

我们可以采用懒汉式

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class LazySimpleSingleton {
    private static LazySimpleSingleton instance;
    private LazySimpleSingleton() {
    }
    public static LazySimpleSingleton getInstance() {
        if (instance == null) {
            instance = new LazySimpleSingleton();
        }
        return instance;
    }
}

懒汉式在类加载的时候不会初始化单例

只有被被外部调用的时候才会创建

下面我们测试一下多线程情况下,这种单例的实现方式能否安全

写一个实现了Runable接口的类

在run方法中调用单例模式的实例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * @author daijiyong
 */
public class TestLazySingletonThread implements Runnable {
    public void run() {
        System.out.println(Thread.currentThread().getName() + LazySimpleSingleton.getInstance());
    }
}

编写主函数,写三个线程进行测试

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * @author daijiyong
 */
public class Test {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new TestLazySingletonThread());
        Thread thread2 = new Thread(new TestLazySingletonThread());
        Thread thread3 = new Thread(new TestLazySingletonThread());
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

显而易见,多线程下并没有保证线程安全

创建了多个单例的对象

其中线程1用的是一个实例

线程0和线程2用的另外一个实例

为什么会出现这个情况呢?

首先我们得知道,Java中线程类的start()方法,并不是会立马执行当前线程

仅仅是告知cpu,你需要执行当前线程,具体什么时候执行,看心情

所以虽然我们是按照thread1、thread2、thread3的顺序执行的start()方法,但是执行顺序却并一定是这样的

而且他们三个的执行顺序可能在不同的时间点,也是不一样的,完全随缘

所以,即便打印出来的两个线程的实例是一样的,也不代表这个单例只被创建了一次

也有可能是创建了两个实例,但是在返回结果之前,第二个实例已经创建完了,将第一个实例覆盖了

怎么优化?第一个想到的应该就是加锁

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class LazySimpleSingleton {
    private static LazySimpleSingleton instance;
    private LazySimpleSingleton() {
    }
    public synchronized static LazySimpleSingleton getInstance() {
        if (instance == null) {
            instance = new LazySimpleSingleton();
        }
        return instance;
    }
}

这样随便执行,就不会出现线程不安全的问题了

但是当我这打了一个断点,查看三个进程执行情况的时候发现了下面的问题

线程0是执行runing状态,但是其他两个线程是监听monitor状态

当执行的线程特别多的时候,会就导致有大量的线程处于等待监听状态

synchronized关键字,是解决了线程安全问题

但是导致在同一个时间只能有一个调用,性能会极速下降

为了解决这个,我们还有一个办法

双重检验方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton instance;
    private LazyDoubleCheckSingleton() {
    }
    public static LazyDoubleCheckSingleton getInstance() {
        //是否要线程阻塞
        if (instance == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                //是否要创建实例
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

第一个检验是为了判断,是否已经初始化了单例

如果已经创建了,则不进行线程阻塞,直接返回

第二个检验是为了判断,在没有初始化的情况下,需要初始化,此时是否因为线程阻塞的原因,已经初始化了

如果已经初始化,则直接返回,如果没有,则进行线程初始化

除了双重检验,还有一个更好的方法来实现懒汉模式

静态内部类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class LazyStaticInnerClassSingleton {
    private LazyStaticInnerClassSingleton() {
        //防止通过Java反射机制创建实例
        if (LazyHolder.INSTANCE != null) {
            throw new RuntimeException("不允许非法访问");
        }
    }
    private static LazyStaticInnerClassSingleton getInstance() {
        return LazyHolder.INSTANCE;
    }
    private static class LazyHolder {
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }
}

这个看起来是饿汉式的写法

但是其实是懒汉式的

这个主要是利用了java类加载的时候,默认是不会加载内部类的机制

只有在调用使用内部类的时候才会对内部类实现初始化

 classloader 机制保证了初始化 instance 时只有一个线程

这样就很好的解决了线程不安全的问题

优雅、高级、装*

3. 枚举式单例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public enum EnumSingleton {
    /**
     * 实例
     */
    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法

它更简洁,利用枚举类自身特点

不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化

为什么呢?

我们可以看一下源码

从Java的源码可以看到

Java在底层就禁止了通过反射的方法创建枚举对象

所以在原生层面就解决了反射机制破坏单例模式的问题

那枚举类是如何保证线程安全的呢?

这个问题问的好,我们在看看源码

由此可知,枚举类在加载的时候

会将每一个枚举类的实例元素放到一个Map当中

这个操作在程序启动、类加载的时候就完成了

之后每次取对象,都是从map中拿的

所以绝对线程安全

但是枚举方式跟饿汉方式一样

是存在内存浪费的情况的

为了解决这个问题,还有一种写法

4. 容器式单例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ContainerSingleton {
    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>(16);
    private ContainerSingleton() {
    }
    public static Object getInstance(String className) {
        if (!ioc.containsKey(className)) {
            try {
                synchronized (ioc) {
                    Object instance = Class.forName(className).newInstance();
                    ioc.put(className, instance);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return ioc.get(className);
    }
}

将每一个实例都缓存到统一的容器当中,使用唯一标识获取实例

当容器中不存在时,则对容器加锁并创建,之后返回

如果已经存在,则直接返回

使用这种方法可以完美的解决多线程和反射带来的问题

5. ThreadLocal单例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
            new ThreadLocal<ThreadLocalSingleton>() {
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };
    private ThreadLocalSingleton() {
    }
    public static ThreadLocalSingleton getInstance() {
        return threadLocalInstance.get();
    }
}

ThreadLocal单例能够保证在一个线程内部的全局唯一,天生线程安全

跨线程的时候,保证不是同一个单例实例

这种实现方式的应用场景非常清晰了

用在多线程中,保证在一个线程中的单例实现

6. 总结

单例模式

优点:

减少内存开销

避免对资源的多重占用

设置全局访问点,严格控制访问

缺点:

没有接口,扩展困难,如果要扩展,只能修改代码,违背了开闭原则

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 你好戴先生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Spark SQL/Hive实用函数大全
本篇文章主要介绍Spark SQL/Hive中常用的函数,主要分为字符串函数、JSON函数、时间函数、开窗函数以及在编写Spark SQL代码应用时实用的函数算子五个模块。
大数据学习与分享
2020/12/31
5.5K0
Hive的利器:强大而实用的开窗函数
与聚合函数类似,开窗函数也是对行集组进行聚合计算。但是它不像普通聚合函数那样,每组通常只返回一个值,开窗函数可以为每组返回多个值,因为开窗函数所执行聚合计算的行集组是窗口。
大数据学习与分享
2021/04/22
3.7K0
Hive的利器:强大而实用的开窗函数
Hive SQL 大厂必考常用窗口函数及相关面试题
二、窗口函数的基本用法 1.基本语法 2.设置窗口的方法 1)window_name 2)partition by 子句 3) order by子句 4)rows 指定窗口大小 3.开窗函数中加order by 和 不加 order by的区别
王知无-import_bigdata
2022/11/11
4.1K0
Hive SQL 大厂必考常用窗口函数及相关面试题
SQL系列(一)快速掌握Hive查询的重难点
作为一名数(取)据(数)分(工)析(具)师(人),不得不夸一下SQL,毕竟凭一己之力养活了80%的数据分析师,甚至更多。SQL语言短小精悍,简单易学,而且分析师重点只关注查询,使得学习成本和时间成本瞬间就下来了。
HsuHeinrich
2023/02/24
3.5K0
SQL系列(一)快速掌握Hive查询的重难点
SQL优化一(SQL使用技巧)
1、行列转换:   decode(条件,值1,返回值1,值2,返回值2,...值n,返回值n,缺省值);   select decode(sign(变量1-变量2),-1,变量1,变量2) from dual; --取较小值   sign()函数根据某个值是0、正数还是负数,分别返回0、1、-1   例如:   变量1=10,变量2=20   则sign(变量1-变量2)返回-1,decode解码结果为“变量1”,达到了取较小值的目的。 举例:查询emp表中的每个部门的人数? SELECT sum(deco
JMCui
2018/03/15
2.8K0
SQL优化一(SQL使用技巧)
Hive 中的排序和开窗函数
排序操作是一个比较常见的操作,尤其是在数据分析的时候,我们往往需要对数据进行排序,hive 中和排序相关的有四个关键字,今天我们就看一下,它们都是什么作用。
大数据真好玩
2021/09/18
2K0
Hive 中的排序和开窗函数
不要到处翻了 | Hive开窗函数总结与实践
平常我们使用 hive或者 mysql时,一般聚合函数用的比较多。但对于某些偏分析的需求,group by可能很费力,子查询很多,这个时候就需要使用窗口分析函数了~ 注:hive、oracle提供开窗函数,mysql8之前版本不提供,但Oracle发布的 MySQL 8.0版本支持窗口函数(over)和公用表表达式(with)这两个重要的功能!
大数据真好玩
2021/01/26
6.5K0
一文学完所有的Hive Sql(两万字最全详解)
lateral view用于和split、explode等UDTF一起使用的,能将一行数据拆分成多行数据,在此基础上可以对拆分的数据进行聚合,lateral view首先为原始表的每行调用UDTF,UDTF会把一行拆分成一行或者多行,lateral view在把结果组合,产生一个支持别名表的虚拟表。
五分钟学大数据
2021/04/02
3.3K0
SQL踩坑经验 | 用SQL实现帕累托累计和滚动平均值
分享一个最近做得比较复杂的东西,总的来说对专业的ETL可能不难,但对我而言稍微有点麻烦的SQL取数需求。
做数据的二号姬
2025/07/04
1060
SQL踩坑经验 | 用SQL实现帕累托累计和滚动平均值
2-3 T-SQL函数
在Transact-SQL语言中,函数被用来执行一些特殊的运算以支持SQL Server的标准命令。SQL Server包含多种不同的函数用以完成各种工作,每一个函数都有一个名称,在名称之后有一对小括号,如:gettime( )表示获取系统当前的时间。大部分的函数在小括号中需要一个或者多个参数。Transact-SQL 编程语言提供了四种函数:行集函数、聚合函数、Ranking函数、标量函数。
py3study
2020/01/08
1.7K0
hive sql(十)—— 编写sql语句实现每班前三名,分数一样并列, 同时求出前三名按名次排序的分差
需求 编写sql语句实现每班前三名,分数一样并列, 同时求出前三名按名次排序的分差 建表语句 create table student( sid string,--学号 cid string,--班级号 score string -- 分数 ) row format delimited fields terminated by '\t' ; 数据 #说明: 数据1具有偶然性,适合不重复的情况,实现可以用扩展部分写法1实现 数据2具有通用性,适合重复数据的情况 #数据1 inser
大数据最后一公里
2021/08/05
8600
大数据学习之数据仓库代码题总结上
请编写 SQL 查询,计算从注册当天开始的每个用户在注册后第1天、第3天、第7天的学习留存率。留存率的计算方式是在注册后的特定天数内继续学习的用户数除以当天注册的用户总数。结果应包含日期、留存天数和留存率。
bxia的厨房_公众号
2024/03/05
3591
大数据学习之数据仓库代码题总结上
Hive 高频考点讲解
Hive 是 FaceBook 开源的一款基于 Hadoop 数据仓库工具,它可以将结构化的数据文件映射为一张表,并提供类SQL查询功能。
sowhat1412
2022/09/20
1.2K0
Hive 高频考点讲解
SQL | 数据分析面试必备SQL语句+语法
前些天在网上冲浪的时候看到一个案例咨询,问说世界500强的数据分析要不要去,评论区一片爆炸:“楼主能分享一下文科生怎么转行做数据分析吗??”、“SQL、python这些学起来好痛苦!”我看着屏幕苦笑,数据分析岗位现在的热门程度如果要形容的话,基本就是随便抓一个微博网友都知道这个岗位了。
咸鱼学Python
2020/03/24
3.4K0
SQL |  数据分析面试必备SQL语句+语法
hive sql(六)—— 每个用户连续登录最大天数
分析中第3点在hive sql系列(三)中计算连续日活中也用到了日期差值,参考链接:
大数据最后一公里
2021/08/05
3.2K0
快速学习-Hive查询
https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Select 查询语句语法:
cwl_java
2020/02/21
2K0
快速学习-Hive查询
最强最全面的Hive SQL开发指南,超四万字全面解析!
hive -S -e 'select table_cloum from table' -S,终端上的输出不会有mapreduce的进度,执行完毕,只会把查询结果输出到终端上。
五分钟学大数据
2021/12/02
8.2K0
最强最全面的Hive SQL开发指南,超四万字全面解析!
3小时吃掉HiveSQL:从基础语法到性能优化
处理数据的标准语言是SQL,常见的包括MySQL/HiveSQL/SparkSQL/Trino,它们的语法基本相差不大。(本教程以HiveSQL语法为主)
lyhue1991
2025/07/30
2510
3小时吃掉HiveSQL:从基础语法到性能优化
spark、hive中窗口函数实现原理复盘
这篇文章从一次业务中遇到的问题出发,深入聊了聊hsql中窗口函数的数据流转原理,在文章最后针对这个问题给出解决方案。
数据仓库践行者
2020/04/20
3.3K0
Hive常用窗口函数实战
本文介绍了Hive常见的序列函数,排名函数和窗口函数。结合业务场景展示了Hive分析函数的使用
Eights
2020/07/13
2.9K0
相关推荐
Spark SQL/Hive实用函数大全
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验