单例模式:最简单的设计模式之一。其作用是保证每个类只有一个实例。使用这一设计模式的目的主要是“统一”,即防止在多实例情况下不同对象内部属性不同,造成访问不同实例时其行为和结果不统一而产生的问题。除此之外,使用单例模式也可以起到节约内存资源的作用。交由Spring框架管理的bean默认模式下都是单例模式。
举一个维基百科上的例子:

许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

维基百科也说明了单例模式的实现思路和注意事项:

实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。

从上述内容可以总结出单例模式的写法思路:
1、获得实例的静态方法
2、私有的构造方法
3、应对多线程环境的互斥设计

Eager模式(饿汉式)

public class EagerSingleton {private static EagerSingleton instance = new EagerSingleton();private EagerSingleton() {}public static EagerSingleton getInstance() {return instance;}
}

这个类的实例在类被加载时才会创建,而ClassLoader的特性保证了这个类只会被加载一次,保证了多线程情况下的安全性。因此这是最简单的单例模式的写法,在大部分情况下也是合适的。

Lazy模式(懒汉式)

public class LazySingleton {private static volatile LazySingleton instance;private LazySingleton() {}public static LazySingleton getInstance() {if (instance == null) {synchronized(LazySingleton.class) {if (instance == null) {instance = new LazySingleton();}}}return instance;}
}

这种写法可以保证instance只在第一次调用getInstance方法时才会被实例化。不过相比于饿汉式,懒汉式写法要繁琐很多,当然其实要点也不难记,只需要记住:①getInstance方法需要使用double check+synchronized(即双重检查锁)的方式保证多线程环境下的互斥并且兼顾并发性能 ②instance一定要用volatile关键字修饰
第①点比较容易理解,第②点则是因为volatile关键字可以保证变量的可见性以及防止指令重排序,下面展开说一下。

instance = new LazySingleton();

虽然只是一行代码,但是在字节码层面实际分为3个步骤
1、分配空间——在堆内存中开辟一块区域用于放置LazySingleton实例
2、初始化——初始化LazySingleton实例
3、引用赋值——将LazySingleton实例的引用赋值给成员变量instance
通过idea上的jclasslib插件可以看到getInstance方法的字节码内容如下:

 0 aload_01 getfield #2 <com/chaltang/singleton/LazySingleton.instance : Lcom/chaltang/singleton/LazySingleton;>4 ifnonnull 18 (+14)7 aload_08 new #3 <com/chaltang/singleton/LazySingleton>
11 dup
12 invokespecial #4 <com/chaltang/singleton/LazySingleton.<init> : ()V>
15 putfield #2 <com/chaltang/singleton/LazySingleton.instance : Lcom/chaltang/singleton/LazySingleton;>
18 aload_0
19 getfield #2 <com/chaltang/singleton/LazySingleton.instance : Lcom/chaltang/singleton/LazySingleton;>
22 areturn

编号8、12、15的三个指令分别代表了1、2、3这三步。在CPU的实际执行中,以及JIT即时编译器的即时编译下,2、3两步由于没有先后关系的要求,就可能出现1、3、2的执行顺序。在单线程环境下,这样其实无所谓,因为这种执行顺序的后果无非是JVM先将一个未初始化的LazySingleton实例的引用赋值给了instance对象,然后再将LazySingleton实例初始化而已。但是在多线程环境下,就有可能出现这种情况:线程T1刚好按照1、3的顺序执行完了第3步,将一个未初始化的空白对象赋值给instance变量之后,CPU时间片到期,线程T2执行getInstance方法,发现instance!=null,于是将instance返回给调用者,调用者执行LazySingleton类的其它成员方法,但由于LazySingleton实例并未初始化,进而发生某些不可预知的错误。
使用volatile关键字修饰instance变量的目的便是使用其防止指令重排序的功能,只要保证1、2、3的执行顺序,就可以防止上述问题的发生。

静态内部类模式

这个模式同样利用了ClassLoader类加载器的特性保证线程安全,这种写法相对Eager模式的好处就是由于SingletonHolder仅仅作为instance的容器存在,理论上正常情况下不会因为getInstance()方法被调用之外的其它原因而被初始化,也就保证了instance只会在getInstance()方法被调用时才会被创建

public class InnerClassSingleton {private static class SingletonHolder {private static InnerClassSingleton instance = new InnerClassSingleton();}private InnerClassSingleton() {}public InnerClassSingleton getInstance() {return SingletonHolder.instance;}
}

枚举类模式

我们知道枚举类中每个代表常量的实例都是单例的,那么我们定义一个枚举类,并且其中只定义一个常量,那么就获得了一个天然实现了单例模式的类。

public enum EnumSingleton {INSTANCE;
}

但是我个人非常不推荐用这种方法实现单例模式,因为这种方式违背了枚举类被创造的初衷。就跟JAVA语言的格式和命名规范一样,虽然语法中没有强制规定,但是大家都默认了类名用大驼峰,变量名用小驼峰模式。枚举类就应该被用来当做一种实现【枚举】功能的常量类来使用,而不应该被当做一种实现单例模式的旁门左道。

进阶-防止反射攻击

我们知道,JAVA里反射可以射一切,我们虽然使用私有化构造方法的方式防止单例模式类被多次实例化,但通过反射是可以绕过这一限制,实例化多个单例对象的。以前面提到的Eager模式为例:

public class ReflectionAttack {public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {Constructor<EagerSingleton> constructor = EagerSingleton.class.getDeclaredConstructor();constructor.setAccessible(true);EagerSingleton reflectInstance = constructor.newInstance();EagerSingleton instance = EagerSingleton.getInstance();System.out.println(instance == reflectInstance);}
}

代码的执行结果是false:

如果通过反射方式实例化的是枚举类,则会报错:

public class ReflectionAttackEnum {public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);constructor.setAccessible(true);EnumSingleton instance = constructor.newInstance("INSTANCE", 0);System.out.println(instance);}
}

进到报错处看一下源码,可以发现原来JDK的开发者早已考虑到这一情况,如果发现反射实例化的是枚举类,便会抛出异常:

枚举类有JDK的保护,我们自己写的类如果想要防止反射攻击,就只能靠自己了:

public class EagerSingleton {private static EagerSingleton instance = new EagerSingleton();private EagerSingleton() {if (instance != null) {throw new RuntimeException("Cannot reflectively create singleton objects");}}public static EagerSingleton getInstance() {return instance;}
}

通过在构造方法中加入instance是否为null的判断,便可以阻止攻击者通过反射调用私有构造方法实例化单例类的尝试。因为反射需要先加载类,而类加载之后就会先实例化instance,后续构造方法再被调用时,只可能是通过反射调用的。
当然,在我们一般的业务开发中是不需要这么写的。因为只要服务器的运行环境安全,我们并不需要这样处心积虑地防止反射攻击。而如果攻击者已经可以在服务器上执行恶意反射代码了,仅仅这一处的防反射设计也只是杯水车薪罢了。

进阶-支持序列化

有时,我们的对象需要进行网络传输,此时需要类支持序列化。对于一般类,支持Serializable接口并指定一个序列化ID即可。但是单例模式的类仅做到这一步还不够:

public class EagerSingleton implements Serializable {private static final long serialVersionUID = -6824975464427476492L;private static EagerSingleton instance = new EagerSingleton();private EagerSingleton() {if (instance != null) {throw new RuntimeException("Cannot reflectively create singleton objects");}}public static EagerSingleton getInstance() {return instance;}
}public class SerializeTest {public static void main(String[] args) throws IOException, ClassNotFoundException {File file = new File("serialized");FileOutputStream fos = new FileOutputStream(file);ObjectOutputStream oos = new ObjectOutputStream(fos);// 先将instance序列化为文件EagerSingleton instance = EagerSingleton.getInstance();oos.writeObject(instance);oos.close();// 再从文件反序列化为对象ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));EagerSingleton obj = (EagerSingleton) ois.readObject();ois.close();// 比较两个对象是不是同一个对象System.out.println(instance == obj);}
}


可以看到上述代码的执行结果是false,单例模式被破坏。怎么解决这一问题呢?JDK设计者已经考虑到了。我们看一下Serializable的接口文档:

Classes that need to designate a replacement when an instance of it is read from the stream should implement this special method with the exact signature.
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
简单翻译:那些需要替换从流中读取出来的实例的类,需要实现下面的方法
任意访问修饰符 Object readResolve() throws ObjectStreamException;

将EagerSingleton的代码改成下面的样子,再次运行SerializeTest,结果就是true了。

public class EagerSingleton implements Serializable {private static final long serialVersionUID = -6824975464427476492L;private static EagerSingleton instance = new EagerSingleton();private Object readResolve() throws ObjectStreamException {return getInstance();}private EagerSingleton() {if (instance != null) {throw new RuntimeException("Cannot reflectively create singleton objects");}}public static EagerSingleton getInstance() {return instance;}
}


ObjectInputStream#readObject()方法是怎么调用到我们写的readResolve()方法的,并不在本文的讨论范围内,读者可以自行debug看一下调用逻辑,或者百度一下网上的文章。
关键是需要记住,单例模式并且需要序列化、反序列化的类,需要实现readResolve()即可。

JAVA单例模式小结相关推荐

  1. Java单例模式--懒汉式和饿汉式(Demo)

    你好我是辰兮,很高兴你能来阅读,本篇文章为大家讲解Java单例模式,相关的更多面试知识已经提前整理好文章可以阅读学习,分享获取新知,希望对Java初学者有帮助. 1.JAVA基础面试常考问题 : JA ...

  2. Java单例模式个人总结(实例变量和类变量)

    Java单例模式 背景知识:Static关键字. 在对于定义类的变量,分为两种,是否具有static修饰的变量: 没有static修饰的变量,通过类的实例化(对象)引用,改变量称为实例变量: 使用st ...

  3. Java中文编码小结

    Java中文编码小结 1. 只有 字符到字节 或者 字节到字符 的转换才存在编码转码; 2. Java String 采用 UTF-16 编码方式存储所有字符.unicode体系采用唯一的码点表示唯一 ...

  4. Java 单例模式探讨

    以下是我再次研究单例(Java 单例模式缺点)时在网上收集的资料,相信你们看完就对单例完全掌握了 Java单例模式应该是看起来以及用起来简单的一种设计模式,但是就实现方式以及原理来说,也并不浅显哦. ...

  5. Java单例模式优化写法

    转载自 http://blog.csdn.net/diweikang/article/details/51354982 Java单例模式优化写法 方法一:推荐 [java] view plain co ...

  6. Java单例模式的几种实现方式

    Java单例模式的几种实现方式 在Java 中,单例类只能有一个实例,必须创建自己的唯一实例,单例类必须给所有其他对象提供这一实例.Java 单例模式有很多种实现方式,在这里给大家介绍单例模式其中的几 ...

  7. Java 单例模式:懒加载(延迟加载)和即时加载

    Java 单例模式:懒加载(延迟加载)和即时加载 引言 在开发中,如果某个实例的创建需要消耗很多系统资源,那么我们通常会使用惰性加载机制(或懒加载.延时加载),也就是说只有当使用到这个实例的时候才会创 ...

  8. Java单例模式:为什么我强烈推荐你用枚举来实现单例模式

    写在前面--原作的这篇文章真的写的非常的简洁,逻辑清晰,将Java单例模式的各种写法写的非常清楚,并介绍了用枚举实现单例的最佳实践. 单例模式简介 单例模式是 Java 中最简单,也是最基础,最常用的 ...

  9. java单例模式 三种_三种java单例模式概述

    在java语言的应用程序中,一个类Class只有一个实例存在,这是由java单例模式实现的.Java单例模式是一种常用的软件设计模式,java单例模式分三种:懒汉式单例.饿汉式单例.登记式单例三种.下 ...

最新文章

  1. phpexcel设置AAA单元格,兼容大于702列数据
  2. 东方明珠胡俊:「东方明珠数据中台」四年发展历史全解(内附彩蛋)
  3. 员工转正申请书_员工有了归属感 企业实现大发展!通机股份在党工共建中摸索经验...
  4. zip解压mysql安装图解_Mysql安装教程-zip格式压缩包
  5. Luogu3092 [USACO13NOV]没有找零No Change (状压DP)
  6. [原创]我的作品:我的迷宫小游戏Java版本
  7. linux分区后盘符找不到,为什么我的磁盘不见了,怎么找回来啊?
  8. 使用jedis访问redis
  9. 浏览器兼容性问题汇总
  10. JavaScript的Forms验证-Parsley.js
  11. 2022年电工(技师)考试报名及电工(技师)复审考试
  12. python从字符串中提取数字
  13. Dao层和Service层的区别
  14. Qt笔记11:qt如何设置应用程序图标和可执行程序图标
  15. 真实骑手数据:73万大学毕业生在送外卖
  16. Eclips配置模板消息
  17. java erp_用Java如何实现ERP系统?
  18. 查看Linux系统有几块网卡
  19. overall accuracy 总体精度的计算
  20. python中import random_Python代码中的“import random”是什么意思?

热门文章

  1. MOCO----Momentum Contrast
  2. ubuntu发送使用sendmail发送邮件
  3. 查询库中所有的表名及数据量
  4. Bonjour (苹果电脑公司的服务器搜索协议)
  5. 专业现场媒体编辑工具:QLab Pro
  6. 笔记本c盘满了怎么清理呢?笔记本c盘清理会误删吗?
  7. P5664 [CSP-S2019] Emiya 家今天的饭
  8. Jmeter运行原理
  9. Synopsys coreConsultant
  10. AT97SC3205 安全芯片介绍