你真的会写单例模式吗?

【WRF-Chem工具】pyVPRM 计算大气与陆地植被之间的 CO₂ 交换通量 pyVPRM 计算大气与陆地植被之间的 CO₂ 交换通量 阅读详情

作者:吃桔子的攻城狮 来源:http://www.tekbroaden.com/singleton-java.html

单例模式可能是代码最少的模式了,但是少不一定意味着简单,想要用好、用对单例模式,还真得费一番脑筋。本文对 Java 中常见的单例模式写法做了一个总结,如有错漏之处,恳请读者指正。

饿汉法

顾名思义,饿汉法就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。代码如下:

public class Singleton {  
    private static Singleton = new Singleton();
    private Singleton() {}
    public static getSignleton(){
        return singleton;
    }
}

这样做的好处是编写简单,但是无法做到延迟创建对象。但是我们很多时候都希望对象可以尽可能地延迟加载,从而减小负载,所以就需要下面的懒汉法:

单线程写法

这种写法是最简单的,由私有构造器和一个公有静态工厂方法构成,在工厂方法中对 singleton 进行 null 判断,如果是 null 就 new 一个出来,最后返回 singleton 对象。这种方法可以实现延时加载,但是有一个致命弱点:线程不安全。如果有两条线程同时调用 getSingleton() 方法,就有很大可能导致重复创建对象。

public class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static Singleton getSingleton() {
        if(singleton == null) singleton = new Singleton();
        return singleton;
    }
}

考虑线程安全的写法

这种写法考虑了线程安全,将对 singleton 的 null 判断以及 new 的部分使用 synchronized 进行加锁。同时,对 singleton 对象使用 volatile 关键字进行限制,保证其对所有线程的可见性,并且禁止对其进行指令重排序优化。如此即可从语义上保证这种单例模式写法是线程安全的。注意,这里说的是语义上,实际使用中还是存在小坑的,会在后文写到。

public class Singleton {
    private static volatile Singleton singleton = null;
    private Singleton(){}
    public static Singleton getSingleton(){
        synchronized (Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }    
}

兼顾线程安全和效率的写法

虽然上面这种写法是可以正确运行的,但是其效率低下,还是无法实际应用。因为每次调用 getSingleton() 方法,都必须在 synchronized 这里进行排队,而真正遇到需要 new 的情况是非常少的。所以,就诞生了第三种写法:

public class Singleton {
    private static volatile Singleton singleton = null;
    private Singleton(){}
    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }    
}

这种写法被称为 “双重检查锁”,顾名思义,就是在 getSingleton() 方法中,进行两次 null 检查。看似多此一举,但实际上却极大提升了并发度,进而提升了性能。为什么可以提高并发度呢?就像上文说的,在单例中 new 的情况非常少,绝大多数都是可以并行的读操作。因此在加锁前多进行一次 null 检查就可以减少绝大多数的加锁操作,执行效率提高的目的也就达到了。

坑

那么,这种写法是不是绝对安全呢?前面说了,从语义角度来看,并没有什么问题。但是其实还是有坑。说这个坑之前我们要先来看看 volatile 这个关键字。其实这个关键字有两层语义。第一层语义相信大家都比较熟悉,就是可见性。可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中。顺便一提,工作内存和主内存可以近似理解为实际电脑中的高速缓存和主存,工作内存是线程独享的,主存是线程共享的。volatile 的第二层语义是禁止指令重排序优化。大家知道我们写的代码(尤其是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同。这在单线程看起来没什么问题,然而一旦引入多线程,这种乱序就可能导致严重问题。volatile 关键字就可以从语义上解决这个问题。

注意,前面反复提到 “从语义上讲是没有问题的”,但是很不幸,禁止指令重排优化这条语义直到 jdk1.5 以后才能正确工作。此前的 JDK 中即使将变量声明为 volatile 也无法完全避免重排序所导致的问题。所以,在 jdk1.5 版本前,双重检查锁形式的单例模式是无法保证线程安全的。

静态内部类法

那么,有没有一种延时加载,并且能保证线程安全的简单写法呢?我们可以把 Singleton 实例放到一个静态内部类中,这样就避免了静态实例在 Singleton 类加载的时候就创建对象,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的:

public class Singleton {
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
    private Singleton(){}
    public static Singleton getSingleton(){
        return Holder.singleton;
    }
}

但是,上面提到的所有实现方式都有两个共同的缺点:

都需要额外的工作 (Serializable、transient、readResolve()) 来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。可能会有人使用反射强行调用我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。枚举写法 当然,还有一种更加优雅的方法来实现单例模式,那就是枚举写法:

public enum Singleton {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java 推荐尽可能地使用枚举来实现单例。

总结

这篇文章发出去以后得到许多反馈,这让我受宠若惊,觉得应该再写一点小结。代码没有一劳永逸的写法,只有在特定条件下最合适的写法。在不同的平台、不同的开发环境(尤其是 jdk 版本)下,自然有不同的最优解(或者说较优解)。比如枚举,虽然 Effective Java 中推荐使用,但是在 Android 平台上却是不被推荐的。在这篇 Android Training 中明确指出:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

再比如双重检查锁法,不能在 jdk1.5 之前使用,而在 Android 平台上使用就比较放心了(一般 Android 都是 jdk1.6 以上了,不仅修正了 volatile 的语义问题,还加入了不少锁优化,使得多线程同步的开销降低不少)。

最后,不管采取何种方案,请时刻牢记单例的三大要点:

  • 线程安全

  • 延迟加载

  • 序列化与反序列化安全

参考资料

  • 《Effective Java(第二版)》

  • 《深入理解 Java 虚拟机——JVM 高级特性与最佳实践(第二版)》

突破Cloudflare 5秒盾:TLS指纹伪装+Playwright行为模拟,爬虫存活率从35%提至78% TLS指纹精准识别:Cloudflare会解析客户端的TLS Client Hello包(如SSL版本、密码套件、扩展字段),Python的requests/urllib库的TLS指纹是“黑名单级”,请求一出即被拦截;行为特征检测:静态的浏览器伪装(如User-Agent)已失效,Cloudflare会监控页面加载后的行为(是否有鼠标移动、滚动、点击,行为是否机械);JS环境深度验证:通过WebGL、Canvas、字体渲染、Navigator属性生成浏览器指纹,无头浏览器的默认配置会被瞬间识别; 阅读详情

相关推荐

EEG | CHB-MIT癫痫数据集CNN分类实战(一)

CHB-MIT癫痫数据集CNN分类实战

Always learn from the best. 1万+

Java基础学习第十二节——单例模式,组合关系

1 课程介绍 1.单例模式(掌握) 单: 一个 例:实例 模式:一种结构 2.代码块(了解) {} 3.组合关系(了解) 2 单例模式 2.1 什么是单例模式 1. 名词解释 单: 一个的意思 例:实例(对象) 模式 就是一种设计模式 需要设计一个类,达到的效果: 此类在整个应用中只存在一个对象(这一个对象可以被多个变量指向的— 开一个房间,给你们每人都配一把钥匙,但是房...

WC名字好难取的博客 351

WIFI基础入门--802.11g--增强速率物理层ERP--15

WIFI基础入门--802.11--增强速率物理层--191.802.11g的组件保护机制 1.802.11g的组件 802.11g其实是将好几种物理层规范合而为一。虽然统称为增强速率物理层(Extended Rate PHY简称ERP),但是实际上ERP有好几种不同的方式: ERP-DSSS与ERP-CCK 这两种模式向下兼容于原始直接序列规范(1Mbps与2Mbps),以及所提到的802.11...

香蕉一号 4619

SignalR 集线器简单实例2

1.路由配置 //注册集线器路由 app.MapSignalR("/realtime", new HubConfiguration() { }); 2.服务端处理 /// /// 集线器定义 /// public class EchoHub : Hub { private static i

天马3798 2233

SEED-VIG数据集实战:5步搞定驾驶员疲劳检测模型(附Python代码)

本文提供了一份基于SEED-VIG数据集的驾驶员疲劳检测模型实战指南。通过五个核心步骤——数据准备、特征提取、模型训练、优化评估及部署思路,详细讲解了如何利用脑电信号构建分类器,并附有完整的Python代码,帮助开发者快速上手这一智能座舱关键技术。

w1x2y3的博客 263

Open3D 进阶(22)间接平差法拟合空间曲线

空间曲线拟合在电力线三维重建中有着广泛的应用,本文利用python使用间接平差法对三维空间曲线的拟合进行代码实现。

点云侠的博客 1059

【转】我为什么不使用单例模式

原文转载自: blog https://ivonhoe.github.io/ x00前言 相信很多同学在学习设计模式的过程中,最先了解的就是单例模式,至少我是这样。单例模式看似简单容易理解,实际上却有很多坑,正因为这样也成为了很多公司面试必考的面试题。实际使用单例模式时,难免会遇到滥用的情况,理解单例更要学会何时拒绝单例。 0x01单例模式的基础知识 1. 单例的几种写法? 懒汉式: 懒加载模式,需要的时候才创建实例。当有多个线程并行调用 getInstance() 的时候,就会创建多.

x73901的博客 675

OpenClaw 运行时错误文档:Embedded Run Agent End with Stream Error(嵌入式运行代理结束流式错误)

OpenClaw运行时错误文档:EmbeddedRunAgentEndwithStreamError(OC-ERR-009) 该错误表示LLM提供商在流式传输过程中发生服务器端异常(HTTP 500),属于可恢复的中等严重性错误。典型表现为日志中出现包含"stream_error"和500状态码的错误信息。系统会自动进行错误分类(50%为服务过载,25%网络问题等),并通过内置机制尝试解决:包括模型切换、API密钥轮换和自动重试策略。用户可采取重试请求、切换模型或简化请求内容等操作,管理

多媒体编程、网络编程、系统编程、网络安全编程、驱动编程 554

单例模式,你真的写对了吗?

看公司代码的时候发现项目中单例模式应用挺多的,并且发现的两处单例模式用的还是不同的方式实现的,那么单例模式到底有几种写法呢?单例模式看似很简单,但是实际写起来却问题多多 本文大纲 什么是单例模式 饿汉式创建单例对象 懒汉式创建单例对象 单例模式的优缺点 单例模式的应用场景 什么是单例模式 确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例,并且有两种创建方式,一种是饿汉式创建,...

wanghao112956的博客 115

C#实战:从零搭建Vosk离线语音转文字系统

本文手把手教你用C#构建离线语音转文字系统,基于NAudio和Vosk实现全流程开发。首先介绍环境搭建,包括NuGet包安装和中文模型配置,强调16kHz单声道音频格式的关键性。接着详细解析音频预处理转换、Vosk模型初始化及识别过程的核心代码,提供多线程优化建议和实时识别扩展思路。文章包含80%以上代码注释,从常见问题排查到性能优化技巧,完整覆盖从"Hello World"到工业级部署的关键技术点,特别适合需要开发本地化语音识别应用的C#开发者。

墨夶的博客 754

机器人轨迹规划——三次样条轨迹1(基于速度的求解方法)

三次样条轨迹是利用三次样条曲线来对给定的一系列点进行插值,从而得到一个光滑的轨迹。三次样条因为每段的多项式次数最高都是三阶的,所以可以得到2阶导数连续,也就是加速度连续的轨迹。假设给定了一系列的点q0q1q2qnq0​q1​q2​...qn​,以及每个点对应的参数t0t1t2tnt0​t1​t2​...tn​,计算一条三次样条曲线sts(t)st,该曲线通过给定的点q0q1q2qnq0​q1。

出门搔白首,若负平生志 1472

你真的会写单例模式吗——Java实现

单例模式可能是代码最少的模式了,但是少不一定意味着简单,想要用好、用对单例模式,还真得费一番脑筋。本文对Java中常见的单例模式写法做了一个总结,如有错漏之处,恳请读者指正。 饿汉法 顾名思义,饿汉法就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。代码如下: public class Singleton { private static Singleton ...

yjz_sdau的博客 238

【经验&技巧】如何在ADS中导入示波器的时域波形?

从示波器中导出的时域波形数据,需要按照固定的格式进行修改,并通过Display dataset对话框导入ADS。

一只豌豆象的博客 1575

你真的会写单例模式吗

目录 饿汉法单线程写法考虑线程安全的写法兼顾线程安全和效率的写法坑静态内部类法枚举写法总结参考资料 转载:你真的会写单例模式吗——Java实现 单例模式可能是代码最少的模式了,但是少不一定意味着简单,想要用好、用对单例模式,还真得费一番脑筋。本文对Java中常见的单例模式写法做了一个总结,如有错漏之处,恳请读者指正。 饿汉法 顾名思义,饿汉法就是在第一次引用该类的时候就创建对象...

记录学习生活的点滴(Linux运维、java) 164

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

你真的会写单例模式吗?

单例模式的介绍,使用饿汉式、懒汉式及DCL单例实现单例模式。介绍volatile关键字在DCL单例中的重要作用。

YonminMa的博客 1242

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

iOS 单例模式,你真的写对了吗?

几年前,笔者曾对 iOS 单例模式作过一番阐述,包括其优点以及 Apple 自身对单例模式的实现示例,详情可参考:iOS 单例模式详解。这里再做一个简短的总结, 单例的用处:主要用在封装网络请求,播放器,存放常用数据等。 单例的特点:只初始化一次,生命周期和程序的生命周期相同,访问方便。 下面一步一步以循序渐进的方式来将单例模式完善化。为了多线程安全,用 GCD 创建单例更加方便,由于 dispa...

黄飞的博客专栏 3749

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

写一个饿汉单例模式的例子_单例模式,你真的写对了吗?

作者:何甜甜在吗来源:juejin.im/post/5d8cc45ae51d4577ef53de12看公司代码的时候发现项目中单例模式应用挺多的,并且发现的两处单例模式用的还是不同的方式实现的,那么单例模式到底有几种写法呢?单例模式看似很简单,但是实际写起来却问题多多。本文大纲什么是单例模式饿汉式创建单例对象懒汉式创建单例对象单例模式的优缺点单例模式的应用场景什么是单例模式确保某个类只有一个实例,...

weixin_39973416的博客 163
上一篇: 再见,Navicat!这个IDEA的兄弟,真香!
下一篇: Java中的注解是如何工作的?
Java思维导图
博客等级 码龄10年 306粉丝 71原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值