java单例模式,其中的细节你注意到了吗
简介
单例模式是应用最广的模式之一,它是为了确保某一个类在一个java虚拟机(进程)中有且只有一个实例存在.
带来的效益:
- 能够实现资源共享,避免由于资源操作时导致的性能或损耗.
- 能够实现资源调度,方便资源之间的互相通信.
- 控制实例产生的数量,达到节约资源的目的.
缺陷 :
- 扩展性差,单例一般没有接口,要扩展只能修改单例类的代码.
- 避免在单例中持有生命周期比单例对象短的引用,容易引起内存泄漏.如Android中的Context对象,需要使用 Application Context代替.
下面介绍单例的七种经典实现方法.
饿汉模式
public class Singleton { // 静态变量初始化, 由于静态变量在类加载过程中,就会被初始化,且类加载又jvm保证线程安全. // 所以这种方式 是线程安全的 private final static Singleton mInstance = new Singleton(); // 构造函数私有化 private Singleton() { // 判断存在则抛出异常, 为了避免反射调用,产生多个实例 if (mInstance != null) throw new RuntimeException("instance exist"); } public static Singleton getInstance() { return mInstance; } }
饿汉模式将变量声明为静态,将在Singleton类被加载的时候,在cinit
阶段进行创建对象,并且是线程安全
的, 类加载过程由JVM来保证线程安全.
饿汉模式能否达到懒加载
我们知道饿汉模式的对象实例是在类加载(初始化阶段)的过程就被创建了,并且并不是所有的类都是在程序启动的时候就加载进内存,那么一个类在什么情况下会被加载或者初始化呢?
这在虚拟机规范中是有严格规定的,虚拟机规范指明 有且只有 五种情况必须立即对类进行初始化:
1 ) 遇到new
、getstatic
、putstatic
或invokestatic
这四条字节码指令
2 ) 使用java.lang.reflect
包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3 ) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4 ) 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。
5 ) 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle
实例最后的解析结果,REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
--------- 引用自 <<深入理解Java虚拟机:JVM高级特性与最佳实践>>
在这五种情况中,其中2,3,4,5 在单例模式中,几乎不会遇到,这里暂不讨论.
我们来看第一种情况,提到的指令分别对应以下操作:
- 外部使用
new
创建该类的对象实例 - 类中的静态变量被外部读取或者设置
- 外部调用了 该类的静态方法
其中1, 我们把构造函数设置为 私有(需要提防 反射和反序列化)
,进本上不会产生问题.
对于2, 我们尽量要避免把变量(除单例变量外)
设为静态且非私有(除非你确定在做什么,不然很可能出现内存浪费或者内存泄漏,毕竟静态变量生命周期和程序一样长
).如果外部调用这样的类变量,将会触发改类初始化.
注释: 静态常量(final static修饰基础类型变量)的调用不会触发类的加载, 该常量会被加入被调用类的常量池中
对于3, 我们单例如果提供静态方法供外部使用,该静态方法被调用时,将也会进行单例类初始化.但是 静态方法,只能调用static
变量,参数变量,以及局部变量,而静态变量在单例中基本上只有 单例本身会声明为静态变量, 总结起来就是, 这个静态方法基本只能达到 工具方法的作用,最好不要声明在单例中.
结论: 饿汉模式不能严格上实现懒加载,除非严格按照要求,不在单例中申明无关的静态变量和静态方法,将也能达到 懒加载的效果.
懒汉模式(线程不安全)
public class Singleton { private static Singleton sInstance = null; private Singleton() { // 防止反射调用,被创建出多个实例 if (sInstance != null) throw new RuntimeException("instance exist"); } // 调用时创建 public static Singleton getInstance() { if (sInstance == null) sInstance = new Singleton(); return sInstance; } }
这种方式能实现懒加载
的目的,并且没有加锁操作,因此线程不安全
,减少了资源的消耗.
在单线程模型下,推荐这种方式的单例, 在多线程模式下 强烈不推荐.
懒汉模式(线程安全)
public class Singleton { private static Singleton sInstance = null; private Singleton() { // 防止反射调用,被创建出多个实例 if (sInstance != null) throw new RuntimeException("instance exist"); } // 调用时创建 public synchronized static Singleton getInstance() { if (sInstance == null) sInstance = new Singleton(); return sInstance; } }
这种方式这种方式能够达到 懒加载
和 线程安全
,但是 它锁住了 整个getInstance()
方法,
对于读的操作if (sInstance == null)
,也进行了加锁,这样对性能有一定的影响.
因此,不大推荐这种方式.
DCL 双重检查锁模式
public class Singleton { // 声明为 volatile 是为了避免在多线程中,new对象时,指令重排, // 造成对象未创建,而判断为非空的情况 private volatile static Singleton sInstance = null; private Singleton() { // 防止反射调用,被创建出多个实例 if (sInstance != null) throw new RuntimeException("instance exist"); } public synchronized static Singleton getInstance() { // 不加锁,判断是否为空, 在锁竞争的情况下,提高性能 if (sInstance == null) { // 只有当为空的时候,加锁创建 synchronized (Singleton.class) { if (sInstance == null) sInstance = new Singleton(); } } return sInstance; } }
这种方式这种方式能够达到 懒加载
和 线程安全
, 并且没有懒汉模式
模式的缺点.它只对'写'(即new
对象)操作进行加锁,判断是否为空时,线程无需等待.
这里需要注意,
sInstance
必须声明为volatile
,不然达不到线程安全. 对象的创建可以拆分为 三条指令,如果对其指令重排就可能出现线程不安全的情况. 具体可以参考笔者的另一篇文章 深入理解 java volatile
因此,比较推荐这种写法.
静态内部类模式
public class Singleton { private Singleton() { // 防止反射调用,被创建出多个实例 if (SingletonHolder.sInstance != null) throw new RuntimeException("instance exist"); } // 当该静态方法被第一次调用时,SingletonHolder类被加载到内存, // 此时,其sInstance变量将会被创建,类加载由jvm保证线程安全 public static Singleton getInstance() { return SingletonHolder.sInstance; } // 类加载时初始化,达到懒加载的目的. // 调用时才被创建 private static class SingletonHolder { private final static Singleton sInstance = new Singleton(); } }
这种方式这种方式能够达到 懒加载
和 线程安全
.
能够实现懒加载,是因为,不管
Singleton
中存不存在其他静态变量或者静态方法,都不会影响到 内部静态类SingletonHolder
, 只有当getInstance()
方法调用时,内部静态类才会被加载,而类加载时,单例被创建实例化. (请对比饿汉模式)
饿汉模式与静态内部类模式对比
饿汉模式
要对类进行约束也能达到懒加载目的. (不适用多余的静态变量和静态方法)
.
静态内部类模式
不需要进行约束就能达到懒加载目的. 但是需要消耗一个内部类的资源来达到目的.(代价很小)
权衡两者, 推荐使用静态内部类方式.
枚举模式
public enum Singleton { INSTANCE; public void method() { // todo ... } }
上述的单例方式都有两个致命的缺点, 不能完全保证单例在jvm中保持唯一性.
- 反射创建单例对象
解决方案 : 在构造上述中判断,当多于一个实例时,再调用构造函数,直接报错.
- 反序列化时创建对象
解决方案 : 使用readResolve()方法来避免此事发生.
这两种缺点虽然都有方式解决,但是不免有些繁琐.
枚举类天生有这些特性.而且实现单例相当简单.
关于枚举类型,能够实现 懒加载
,线程安全
,以及确保单例在jvm中保持唯一性
.
请参考笔者的另一篇文章 java 枚举(enum) 全面解读
因此, 强力推荐
使用这种方式创建单例.
但是由于枚举的使用时,枚举类的装载和初始化时会有更多的时间和空间的成本, 它的实现比其他方式需要更多的内存空间,所以在
Android
这种受资源约束的设备中尽量避免使用枚举单例
总结
- 在单线程下,建议使用
懒汉模式(线程不安全版)
- 多线程,且资源受限(
Android
),建议使用DCL
和静态内部类
版本 - 其他情况,建议使用
枚举
方法
引用
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
yum如何下载安装jdk
yum 是一个方便工具。怎么查看 yum可以安装下载的软件? 命令:yum search java|grep jdk / yum list java*。 查看yum库中有哪些JDK版本 $ yum search java|grep jdk 在CentOS下如何使用yum安装JDK选择1.8版本进行安装 $ yum install java-1.8.0-openjdk 安装完后,默认的安装目录是:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-3.b14.el6_9.x86_64下 在CentOS下如何使用yum安装JDK在/etc/profile文件中配置JAVA相关的环境变量,在结尾添加以下内容: set java environment JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-3.b14.el6_9.x86_64 JRE_HOME=$JAVA_HOME/jre CLASS_PATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools:$JRE_H...
- 下一篇
Centos7已配置环境变量,javac命令执行无效
在centos7中以rpm包安装jdk无需配置环境变量, terminal中输入java -verison及java 命令也是没问题的,但是javac的话就会提示没有此命令 此时我们用yum来装原生的就行了: 使用 yum install java-devel (非管理员账号使用 sudo yum install java-devel命令) 下载安装完毕后,再次输入 javac ,ok了
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Mario游戏-低调大师作品
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS6,CentOS7官方镜像安装Oracle11G
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2更换Tomcat为Jetty,小型站点的福音