个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道
如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充
前言
单例模式是Java最简单和常见的模式之一
都知道Java中,类需要被实例化为对象才能使用,因而同一个类可以被实例化为多个对象,但是部分场景我们需要使用同一个对象,这就是单例模式出现的原因
1.介绍
使用目的:保证一个类仅有一个实例,并提供一个访问他的全局访问点
使用时机:需要节省系统资源,或者需要在多个地方公用类里面的数据等情况
解决问题:频繁创建和销毁实例导致资源浪费,同一个类的不同实例的数据互不公用
实现方法:单例类通过私有构造函数创建唯一实例,并提供公共访问点给其他对象
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
应用实例:
- 全局管理器,如手机app里下载管理器,只能有一个管理器,负责调控真个app的下载任务
- 命令池等公用对象,需要公用里面的数据,那么显然只能创建唯一实例并提供给其他对象
- 消耗资源较多的实例,如后端需要发送http请求,若频繁创建client则会消耗大量资源,那么不妨将该实例作为单例,每次都是用同一个client
优点:
- 仅有一个实例,那么避免了频繁创建和销毁带来的资源消耗
- 实例公用,那么可以保证不同对象可以使用同一份数据
- 实例由类自身持有,那么不会因为无人持有而销毁,从而达到数据持久化的目的
缺点:没有接口,不能继承,自己创建自己的实例,与单一职责原则冲突(一个类应该只关心内部逻辑,而不关心外面怎么样来实例化)
2.实现
2.1.基本步骤
-
定义私有构造函数,那么该类将不会被其他对象实例化
private SingleObject(){}
-
自己创建实例化对象
private static SingleObject instance = new SingleObject();
-
定义全局访问点,提供给其他对象
public static SingleObject getInstance(){ return instance; }
-
定义类相关业务代码
public void showMessage(){ System.out.println("Hello World!"); }
完整代码
public class SingleObject {
//创建 SingleObject 的一个对象
private static SingleObject instance = new SingleObject();
//让构造函数为 private,这样该类就不会被实例化
private SingleObject(){}
//获取唯一可用的对象
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.println("Hello World!");
}
}
-
在其他对象中获取实例,并调用相关业务方法
public class SingletonPatternDemo { public static void main(String[] args) { //不合法的构造函数 //编译时错误:私有构造函数 SingleObject() 不可见的 //SingleObject object = new SingleObject(); //获取唯一可用的对象 SingleObject object = SingleObject.getInstance(); //显示消息 object.showMessage(); } }
2.2.五种常见实现方案
2.2.1.懒汉式
第一次被调用时初始化,节省资源,但线程不安全,
基于“懒“的思想实现,即被动初始化,如果不被使用就不会被初始化
由私有构造器和一个公有静态工厂方法构成,在工厂方法中对singleton进行null判断,如果是null就new一个出来,最后返回singleton对象
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
该方法的优点是节省了资源,如果不需要被使用,就不会初始化,只有第一次使用才会生成实体对象
而缺点很明显,就是线程不安全,如果两个方法同时调用
Singleton.getInstance()
,就可能重复生成对象如果对
Singleton.getInstance()
加上同步锁即可解决线程不安全的问题,但是代价是每次调用都会产生不必要的同步开销,反而相当浪费资源
2.2.2.饿汉式
类加载时初始化,最简单常用,线程安全,但容易浪费资源
基于“饥饿”的思想实现,即主动初始化,无论自己是否真的会被使用
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
优点是线程安全,且获取对象的速度很快
但类加载时初始化无疑会拖慢项目的启动速度,而且自身并不一定会被使用,可能导致资源浪费
2.2.3.双重锁模式
第一次被调用时初始化,线程安全,且在多线程下依然性能优秀
基于“双重检查”机制
- 检查是否已被初始化,没有就创建一个实体对象,避免重复创建实例
- 创建对象时,对类本身进行加锁同步,防止多线程下的重复创建实例
==由于
singleton=new Singleton()
的创建可能被JVM重排序,而导致多线程下的风险,因而使用volatile
修饰signleton
实例变量==
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
线程安全,存取较快,且在多线程下依然不影响性能,可以说是性能最好的一种方法了
但是
volatile
关键词不可避免的影响了部分性能,但这一点点代价是值得的
2.2.4.静态内部类
第一次被调用时初始化,线程安全,且性能优秀
使用一个静态内部类来持有自身实力,因而可以保证实体类的唯一性和线程安全性
public class Singleton {
private Singleton(){
}
public static Singleton getInstance(){
return Inner.instance;
}
private static class Inner {
private static final Singleton instance = new Singleton();
}
}
因为是静态的内部类,所以不用担心多线程问题,且静态类必定唯一
2.2.5.枚举模式
类加载时初始化,简单,线程安全,且能够防止反射入侵和反序列化
默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。实际上
- 枚举类隐藏了私有的构造器。
- 枚举类的域 是相应类型的一个实例对象
public enum Singleton {
INSTANCE;
//可以省略此方法,通过Singleton.INSTANCE进行操作
public static Singleton getInstance() {
return Singleton.INSTANCE;
}
}
枚举模式算是最简单的一种单例模式,但也因此导致可读性较差,实际开发中使用较少
但其防反射入侵和反序列化的特性,受到很多人的推崇,号称最安全实用的单例模式???
3.补充
3.1.双重锁模式与volatile
对于双重锁模式的创建方法
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
第5行的singleton = new Singleton();
实际上包括以下三步
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
由于2和3之间没有依赖关系,其顺序可能会被JVM重排序,而变成下面的顺序
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象
在单线程下不会出现异常,如对于线程A,A2和A3互换顺序并不影响结果,最后得到的结果都是初始化后的对象
但如果在多线程情况下,线程A的A2与A3互换位置,致使==某个瞬间instance不为空,但并没有初始化对象==
此时另一个线程B将会获得一个还未初始化完成的instance,那么就喜闻乐见的空指针了
解决办法就是把instance声明为volatile型,因为被volatile关键字修饰的变量是被禁止重排序的
3.2.单例模式被破坏
单例模式的主要目的就是保持自身==仅有一个实例对象==,那如果出现了多个实例对象呢?这个模式就被破坏了
上述几种实现方法中,我们都隐藏了构造函数,而仅仅暴露一个公共入口,用这个入口来保持仅有一个实例对象
那么只需要跳过这个入口,直接访问构造函数,或者直接访问实例对象,就相当于破坏了这个模式
常见情况有两种
-
映射:隐射能获取一个对象里的所有变量和属性,即便是私有的,那么可以将类进行复制,并将私有构造函数修改为共有的,就可以利用构造函数生成新的实例了,举例如下
单例使用饿汉式单例,测试方法如下
package com.company.test; import java.lang.reflect.Constructor; public class SingletonTest { private static SingletonTest instance = new SingletonTest(); private SingletonTest() { } public void saySomething() { System.out.println("Hello world!"); } public static SingletonTest getInstance() { return instance; } public static void main(String[] args) throws Exception { SingletonTest s1 = SingletonTest.getInstance(); SingletonTest s2 = SingletonTest.getInstance(); Constructor
constructor = SingletonTest.class.getDeclaredConstructor(); constructor.setAccessible(true); SingletonTest s3 = constructor.newInstance(); System.out.println("s1:" + s1 + "\n" + "s2:" + s2 + "\n" + "s3:" + s3); System.out.println("正常情况下,实例化两个实例是否相同:" + (s1 == s2)); System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (s1 == s3)); } } 可以看到两个通过实例提供的公共接入口生成的是同一个实例,而映射复制后生成的并不是同一个
-
序列化:即将对象转换成字节序列,再将其转换回对象,那么转回的对象与原对象是否是同一个呢?显然不是,这样就破坏了单例模式的原则了
依旧使用单例模式,测试方法如下
package com.company.test; import java.io.*; import java.lang.reflect.Constructor; public class SingletonTest implements Serializable { private static SingletonTest instance = new SingletonTest(); private SingletonTest() { } public void saySomething() { System.out.println("Hello world!"); } public static SingletonTest getInstance() { return instance; } public static void main(String[] args) throws Exception { SingletonTest s1 = SingletonTest.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj")); oos.writeObject(s1); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SerEnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); SingletonTest s4 = (SingletonTest) ois.readObject(); ois.close(); System.out.println("s1:" + s1 + "\n" + "s4:" + s4); System.out.println("序列化前后两个是否同一个:" + (s1 == s4)); } }
显然,新的对象也不是旧的实例
上述五种单例模式,都可以被映射和序列化破坏掉,但枚举模式不会
3.3.枚举模式的防破坏机制
上面说了,常见的破坏方法就隐射和序列化,枚举模式天生可以避免这两种情况
-
反射在通过
newInstance()
创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败测试方法同上,单例模式改为枚举模式
package com.company.test; import java.io.*; import java.lang.reflect.Constructor; public enum SingletonTest implements Serializable { INSTANCE; //可以省略此方法,通过Singleton.INSTANCE进行操作 public static SingletonTest getInstance() { return SingletonTest.INSTANCE; } public static void main(String[] args) throws Exception { SingletonTest s1 = SingletonTest.getInstance(); SingletonTest s2 = SingletonTest.getInstance(); Constructor
constructor = SingletonTest.class.getDeclaredConstructor(); constructor.setAccessible(true); SingletonTest s3 = constructor.newInstance(); System.out.println("s1:" + s1 + "\n" + "s2:" + s2 + "\n" + "s3:" + s3); System.out.println("正常情况下,实例化两个实例是否相同:" + (s1 == s2)); System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (s1 == s3)); } } 结果如下,VM抛出异常
-
enum类不能被继承,在反编译的时候可以发现该类是final的,而且enum类有且仅有private的构造器,防止被外部构造
因此可以保证==对于序列化和反序列化,每一个枚举类型和枚举变量在JVM中都是唯一的==,因而序列化和反序列化并不会生成新的实例,也就不会破坏单例模式
测试代码如下
package com.company.test; import java.io.*; public enum SingletonTest implements Serializable { INSTANCE; //可以省略此方法,通过Singleton.INSTANCE进行操作 public static SingletonTest getInstance() { return SingletonTest.INSTANCE; } public static void main(String[] args) throws Exception { SingletonTest s1 = SingletonTest.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj")); oos.writeObject(s1); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SerEnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); SingletonTest s4 = (SingletonTest) ois.readObject(); ois.close(); System.out.println("s1:" + s1 + "\n" + "s4:" + s4); System.out.println("序列化前后两个是否同一个:" + (s1 == s4)); } }
可以看到s1和s4都是
INSTANCE
,是一个唯一常量,序列化和反序列化并没有生成新的实例
4.传送门
https://www.cnblogs.com/chiclee/p/9097772.html
https://www.cnblogs.com/saoyou/p/11087462.html
5.后记
对于五种实现方案,饿汉式是最常用的,而枚举是最被推崇的模式,但个人还是比较喜欢双重锁模式,看情况使用吧,还是不能无视实际场景一慨而论
作者:Echo_Ye
WX:Echo_YeZ
Email :[email protected]
个人站点:在搭了在搭了。。。(右键 - 新建文件夹)