设计模式-单例模式及应用
单例模式
所谓的单例模式,就是采取一定的方法保证在整个系统中,对某个类只有一个对象实例;并且该类只提供一个获取该对象实例的方法。
单例模式三要素
- 构造方法私有化;
- 实例化变量引用私有化;
- 暴露公共方法获取实例。
Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的 getInstance() 工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个 Singleton 类型的静态对象,作为外部共享的唯一实例。围绕所介绍的三要素,下面介绍下单例模式的8中写法,并列举其优缺点。
8种创建单例对象示例
1.饿汉式-静态常量 (线程安全)
class Singleton {
private Singleton() {
}
private final static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
优点:简单,使用时没有延迟;在类装载时就完成实例化,天生的线程安全
缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
2.饿汉式-静态变量 (线程安全)
class Singleton {
private Singleton() {
}
private static Singleton instance;
static {
instance = new Singleton();
}
public static Singleton getInstance() {
return instance;
}
}
优缺点说明:这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类裝载的时候,就执
行静态代码块中的代码,初始化类的实例。优缺点和上面是一样
3.懒汉式(线程不安全)
class Singleton {
private static Singleton instance;
private Singleton() {}
//提供一个静态的公有方法,当使用到该方法时,才去创建 instance
//即懒汉式
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点:起到了懒加载的目的,避免了内存资源的浪费;
缺点:多线程条件下会造成线程不安全现象的发生;在多线程条件下,一个线程进入了if(instance == null)
判断语句,还未往下执行,另外一个线程也通过了这个判断语句,这是便会产生多个实例;在实际开发中不要使用此种方式。
4.懒汉式-同步方法(线程安全)
class Singleton {
private static Singleton instance;
private Singleton() {}
//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点:解决了懒汉式线程安全的问题,通过静态同步方法的方式控制实例对象只创建一次;
缺点:效率太低,每个线程在获取实例时候,要同步的方式获取,不推荐使用此种方式。
5.懒汉式-同步代码块(线程安全)
class Singleton {
private static Singleton instance;
private Singleton() {}
//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleto.class){
instance = new Singleton();
}
}
return instance;
}
}
优缺点同上;
6.双重锁检查(线程安全)
class Singleton {
private static volatile Singleton instance;
private Singleton() {}
//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
//同时保证了效率, 推荐使用
public static synchronized Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
优点:Double-Check 线程安全、延时加载、效率高,推荐使用此种方式。
由于 JVM 具有指令重排的特性,在多线程环境下可能出现 singleton 已经赋值但还没初始化的情况,导致一个线程获得还没有初始化的实例。volatile 关键字的作用:
- 保证了不同线程对这个变量进行操作时的可见性
- 禁止进行指令重排序
7.静态内部类(线程安全)
class Singleton {
//构造器私有化
private Singleton() {}
//写一个静态内部类,该类中有一个静态属性 Singleton
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
public static synchronized Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
此种方式是利用JVM类的装载机制,来保证初始化实例时,只有一个线程;
静态内部类方式在singleton类被装载时不会立即执行实例化,而是在需要实力化时,调用getInstance方法时,才会去装载SingletonInstance类,从而完成Singleton对象的实例化;
类的静态属性只会在第一次类加载的时候初始化,JVM会帮助我们保证线程的安全性,在类进行初始化时,别的线程无法进入;
优点:避免线程不安全,利用静态内部类的特点来实现延迟加载,效率高;推荐使用。
8.枚举(线程安全)
enum Singleton {
INSTANCE;
public void sayHello() {
System.out.println("hello world~~~");
}
}
优缺点:通过JDK1.5中,添加的枚举来实现单例模式,写法简单且能避免多线程同步问题,而且能防止反序列化重新创建新的对象。推荐使用。
单例模式的安全性
单例模式的目标是,任何时候该类都只有唯一的一个对象。但是上面我们写的大部分单例模式都存在漏洞,例如序列化攻击,反射攻击等,被攻击时会产生多个对象,破坏了单例模式。
序列化攻击
回顾Java反序列化主要要点:
-
需要实现
java.io.Serializable
接口,否则会抛出NotSerializableException
异常 -
需要显示声明一个
serialVersionUID
变量,如果没有设置,Java序列化机制会根据编译的class生成一个serialVersionUID
作为序列化版本的比较(主要为了验证其一致性),如果检查到序列化和反序列化serialVersionUID
不同,则抛出异常。 -
当某个字段被声明为
transient
后,默认序列化机制就会忽略该字段,反序列化后自动获得0或者null值,静态成员不参与序列化 -
每个类可以实现
readObject
、writeObject
方法实现自己的序列化策略,即使是transient
修饰的成员变量也可以手动调用ObjectOutputStream
的writeInt
等方法将这个成员变量序列化。 -
任何一个
readObject
方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例 -
每个类可以实现
private Object readResolve()
方法,在调用readObject
方法之后,如果存在readResolve
方法则自动调用该方法,readResolve
将对readObject
的结果进行处理,而最终readResolve
的处理结果将作为readObject
的结果返回。readResolve
的目的是保护性恢复对象,其最重要的应用就是保护性恢复单例、枚举类型的对象.(重点) -
Serializable
接口是一个标记接口,可自动实现序列化,而Externalizable
继承自Serializable
,它强制必须手动实现序列化和反序列化算法,相对来说更加高效
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton singleton = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
// 对象转为二进制流 写入文件(序列化)
oos.writeObject(singleton);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
// 二进制流转为对象 (反序列化)
HungrySingleton newSingleton = (HungrySingleton) ois.readObject();
System.out.println(singleton);
System.out.println(newSingleton);
System.out.println(singleton == newSingleton);
}
}
执行结果:
com.atguigu.singleton.serializableattack.HungrySingleton@279f2327
com.atguigu.singleton.serializableattack.HungrySingleton@4783da3f
false
通过结果发现,反序列化的对象和我们生成的单例对象不是同一个对象。这就没有达到我们单例的目的。
序列化问题解决
通过前面序列化要点介绍,方法readResolve
,在调用readObject
方法之后,如果存在readResolve
方法则自动调用该方法,readResolve
将对readObject
的结果进行处理,而最终readResolve
的处理结果将作为readObject
的结果返回. 那这里我们就重写 readResolve
方法,直接返回instance实例。
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
private Object readResolve() {
System.out.println("readObject after....");
return instance;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton singleton = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
//对象转为二进制流 写入文件(序列化)
oos.writeObject(singleton);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
// 二进制流转为对象 (反序列化)
HungrySingleton newSingleton = (HungrySingleton) ois.readObject();
System.out.println("readResolve after....");
System.out.println(singleton);
System.out.println(newSingleton);
System.out.println(singleton == newSingleton);
}
执行结果:
readObject after....
readResolve after....
com.atguigu.singleton.serializableattack.HungrySingleton@279f2327
com.atguigu.singleton.serializableattack.HungrySingleton@279f2327
true
可以看到此时,返回的对象地址是相同的。
反射攻击
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
HungrySingleton instance = HungrySingleton.getInstance();
Constructor constructor = HungrySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // 获得权限
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
执行结果:
com.atguigu.singleton.reflectattack.HungrySingleton@6f94fa3e
com.atguigu.singleton.reflectattack.HungrySingleton@5e481248
false
使用反射,可以通过Class对象来创建行的指定类的对象实例,在单例模式中构造器都是私有的,而反射可以通过构造器对象调用 setAccessible(true) 来获得权限,这样就可以创建多个对象,来破坏单例模式了,避免通过反射创建单例对象的方法,可以在构造器中加一层判断。
private HungrySingleton() {
// instance 不为空,说明单例对象已经存在
if (instance != null) {
throw new RuntimeException("单例模式禁止反射调用!");
}
}
执行结果:
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.atguigu.singleton.reflectattack.HungrySingleton.main(HungrySingleton.java:30)
Caused by: java.lang.RuntimeException: 单例模式禁止反射调用!
at com.atguigu.singleton.reflectattack.HungrySingleton.<init>(HungrySingleton.java:18)
... 5 more
反射最调的还是构造方法,这里通过饿汉式
的方式,在类加载的时候单例对象已经创建完成,所有这里我们通过构造方法的方式可以直接判断是否存在,来控制外部不能通过反射创建对象。
注意: 而此种方式使用时,需要注意的事仅针对饿汉式,懒汉式的方式不适用,懒汉式是懒加载的方式,那在我们使用之前,不管通过反射创建多少个对象,我们都不清楚。
终极方案枚举
列举上述的种种单例实现方式,有很多漏洞需要我们注意。为什么我们这里推荐使用枚举的方式。
public enum EnumSingleton implements Serializable {
INSTANCE; // 单例对象
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
private EnumSingleton() {
}
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingleton singleton1 = EnumSingleton.INSTANCE;
singleton1.setContent("枚举单例序列化");
System.out.println("枚举序列化前读取其中的内容:" + singleton1.getContent());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
oos.writeObject(singleton1);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
EnumSingleton singleton2 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println(singleton1 + "\n" + singleton2);
System.out.println("枚举序列化后读取其中的内容:" + singleton2.getContent());
System.out.println("枚举序列化前后两个是否同一个:" + (singleton1 == singleton2));
Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
EnumSingleton singleton3 = constructor.newInstance(); // 通过反射创建对象
System.out.println("反射后读取其中的内容:" + singleton3.getContent());
System.out.println("反射前后两个是否同一个:" + (singleton1 == singleton3));
}
执行结果:
枚举序列化前读取其中的内容:枚举单例序列化
INSTANCE
INSTANCE
枚举序列化后读取其中的内容:枚举单例序列化
枚举序列化前后两个是否同一个:true
Exception in thread "main" java.lang.NoSuchMethodException: com.atguigu.singleton.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.atguigu.singleton.EnumSingleton.main(EnumSingleton.java:45)
可以看到,序列化后的对象地址没有变,而且当我们使用反射试图创建对象是,报错。
底层实现,我们可以通过 javap -c **.class
命令,查看字节码文件,即构成 Java类 字节码的指令
## singleton: javap -c EnumSingleton.class
Compiled from "EnumSingleton.java"
public final class com.atguigu.singleton.EnumSingleton extends java.lang.Enum<com.atguigu.singleton.EnumSingleton> implements java.io.Serializable {
public static final com.atguigu.singleton.EnumSingleton INSTANCE;
可以看到,我们通过生成的字节码文件看到,public final class T extends Enum,说明我们将类定义为enume类型时,编译器底层会帮助我们创建一个final类型的类继承Enume类,所以我们的枚举类不能被继承。
枚举能够避免反射的攻击,是因为反射不支持创建枚举类型对象。
综上:枚举的好处可以列举为一下几个点:
- 写法简单,枚举的写法不同于其他几种方式,需要大量的代码;
- 线程安全 单例对象 INSTANCE,通过字节码文件得到,是通过static修饰的,类加载之后初始化JVM可以保证线程安全;
- 懒加载 JAVA类在引用到时,才会去进行类加载。所以枚举单例也具有懒加载的效果;
- 枚举能避免序列化、反射攻击。
单例模式总结
- 单例模式提供了对唯一实例的受控访问;
- 单例模式保证了系统内存中该类只用一个对象,节省了系统资源,对一些需要频繁创建销毁的对象,使用单例可以提升系统性能;
单例模式应用
JDK Runtime (饿汉式)
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {
}
//....
}
JDK Runtime类代表着Java程序的运行时环境,每个Java程序都有一个Runtime实例,该类会被自动创建,我们可以通过 Runtime.getRuntime() 方法来获取当前程序的Runtime实例。一旦得到了一个当前的Runtime对象的引用,就可以调用Runtime对象的方法去控制Java虚拟机的状态和行为。
✨✨ 欢迎🔔 订阅个人的微信公众号
✨✨ 个人GitHub地址