目录
1.单例模式的程序结构
2.饿汉式单例模式的实现
3.饿汉式线程安全
4.防止反射破坏单例
5.防止序列化破坏单例
6.总结
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。所谓单例就是在系统中只有一个该类的实例,并且提供一个访问该实例的全局访问方法。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式,关于单例模式的原理,可参考文末的链接文章。
单例的实现分为三个步骤:
- 构造方法私有化。即不能在类外实例化,只能在类内实例化。
- 在本类中完成自身的初始化,自己创建本类的实例,且是唯一的实例。
- 在本类中提供给外部获取实例的方式,提供访问该实例的全局静态方法getInstance(),来获取该类的唯一实例引用。
单例模式的特点:从系统启动到终止,整个过程只会产生一个实例。因为单例提供了唯一实例的全局访问方法,所以它可以优化共享资源的访问,避免对象的频繁创建和销毁,从而可以提高性能。单例模式常见的应用场景如下:Windows任务管理器、数据库连接池、Java中的Runtime、Spring中Bean的默认生命周期等。
1.单例模式的程序结构
单例模式简化后的类图如下所示:
类图显示了该类的一个私有静态成员变量、一个公有静态方法(又叫静态成员函数)以及一个私有的构造函数。虽然这是简化后的结构,但也是单例模式的主要结构。顺便说一下,这个类图只是个简化后的结构,该类通常还会有其它的非静态的成员变量和方法,当获取到该类的唯一实例后,就在该实例上调用这些其它方法来执行该类提供的功能。
私有的构造函数保证只能在类内部实例化;静态成员变量用来保存该类的唯一实例,该静态成员变量必须是private的,以防止用户可以直接访问到它。如果用户想要访问该单例类的唯一实例,它只能调用该类的静态方法(getInstance)。
注意static(静态成员变量、静态方法)的使用。从语法上来说,创建的单例类是不允许被其他程序用new来创建该对象的,所以只能将这个单例类中的方法定义成静态的,而静态方法又不能去访问非static成员的,所以因此类自定义的实例变量也必须是静态的。
这里不妨回顾一下,静态成员变量是属于整个类的,仅在类的初次加载时初始化,在类被销毁时才会被回收。通过该类实例化的所有对象都共享该静态变量,任一对象对于该静态变量的修改都会影响所有的对象。静态方法同样是属于整个类的,可以通过类名与对象名进行访问,而非静态成员是随着对象的创建而被实例化的。在调用静态方法时,可能对象还没有实例化,自然也就没有对象的非静态成员的实例化,所以无法访问非静态的成员。
2.饿汉式单例模式的实现
在Java中实现单例模式通常有两种形式.:
- 饿汉式:类加载时,就进行对象实例化。
- 懒汉式:第一次引用类时,才进行对象实例化。
这里主要聚焦于饿汉式。饿汉式代码实现如下:
public class HungrySingLeton {
// 创建HungrySingLeton 的一个对象
private static final HungrySingLeton instance = new HungrySingLeton();
// 让构造函数为private,这样该类就不会被实例化
private HungrySingLeton() {
}
// 获取唯一可用的对象
public static HungrySingLeton getInstance(){
return instance;
}
/**
* 获取对象的内存地址
* @return
*/
public long getRamAddress() {
return this.hashCode();
}
}
注意instance变量加了final的,一般建议加final,除非说有释放资源等特殊要求。这种方式简单,也比较常用,在类创建的同时已经创建好一个静态的对象供系统使用,执行效率高。
3.饿汉式线程安全
饿汉式单例通过getInstance获取的单例,在类加载时已经初始化完毕,在多线程环境下也是安全的,所以不需要同步。我们可以通过测试来验证。
先写一个公共方法,用于多线程环境下获取单例:
/**
* 公共方法,在多线程环境下测试单例,避免重复编写测试代码
* @param threadCount 线程数
* @param func 函数,用于获取单例
* @param <T>
*/
public static <T> void singLetonMultiThread(int threadCount, Supplier<T> func) {
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
IntStream.range(0, threadCount).forEach(i -> {
executorService.submit(() -> {
System.out.println(func.get());
});
});
// 等线程全部执行完后关闭线程池
executorService.shutdown();
try {
executorService.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
这个公共方法可以接收一个函数,该函数就是获取单例的方法。
有了公共方法后,接着写测试代码:
public static void hungrySingLetonTest() {
Supplier func = () -> HungrySingLeton.getInstance();
singLetonMultiThread(10, func);
}
测试结果如下:
多线程下获取到的单例始终是同一个对象,他们的内存地址都一样。
4.防止反射破坏单例
到这里,可能会认为已经很OK了。但是,Java还有个反射机制,通过反射,可以轻易破解单例的安全。
public static void reflectionTest() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<HungrySingLeton> clazz = HungrySingLeton.class;
// 获取HungrySingLeton的默认构造函数
Constructor<HungrySingLeton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
// 调用默认构造函数创建实例
HungrySingLeton h1 = constructor.newInstance();
HungrySingLeton h2 = constructor.newInstance();
System.out.println(h1.getRamAddress());
System.out.println(h2.getRamAddress());
}
得到的结果如下:
是两个对象实例!既然反射是先获得class(也是类的实例),再通过calss获得构造函数,去获取单例,那么解决办法就是在饿汉式构造函数中,同步类:
public class HungrySingLeton {
// 创建HungrySingLeton 的一个对象
private final static HungrySingLeton instance = new HungrySingLeton();
// 让构造函数为private,这样该类就不会被实例化
private HungrySingLeton() {
// 防止防止反射破坏单例
synchronized (HungrySingLeton.class) {
if(instance != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
}
// 获取唯一可用的对象
public static HungrySingLeton getInstance(){
return instance;
}
/**
* 获取对象的内存地址
* @return
*/
public long getRamAddress() {
return this.hashCode();
}
}
测试结果如下:
对于饿汉式来说,不管getInstance有没有在反射之前,效果都一样,因为反射前获取到类实例的时候,单例就已经初始化完成了,synchronized (HungrySingLeton.class)都可以检测到,从而防止了反射破坏。
5.防止序列化破坏单例
对单例进行序列化/反序列化也会破坏单例,如下测试:
public static void hungrySingleLettonSerializable() {
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
File file = new File("d:\\Singleton.txt");
// -----------序列化-------------
// 创建输出流
oos = new ObjectOutputStream(new FileOutputStream(file));
//将单例对象写到文件中 序列化
oos.writeObject(HungrySingLeton.getInstance());
oos.flush();
// -----------反序列化-------------
// 从文件读取单例对象
ois = new ObjectInputStream(new FileInputStream(file));
//反序列化得到对象
HungrySingLeton hungrySingLeton= (HungrySingLeton)ois.readObject();
System.out.println(hungrySingLeton == HungrySingLeton.getInstance()); //false
file.deleteOnExit();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(oos != null) {
oos.close();
}
if(ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
测试结果就是false!
为什么序列化/反序列化会破坏单例呢?因为反序列化底层使用了反射,利用无参构造函数创建了一个新的对象obj;然后判断判断该对象obj是否有 readResolve() 方法,如果有的话,则通过反射调用该方法,来获得目标对象,否则就直接返回obj。因为我们的单例中没有readResolve() 方法,所以反序列化出来的对象必然是不同的。
找到原因了,那解决方法也自然有了,就是新增readResolve()方法:
package com.java.concurrency.in.practice.ch02.se22;
import org.openjdk.jol.vm.VM;
import java.io.Serializable;
/**
* 饿汉式单例模式
* @author yedashi
* @version 1.0
* @date 2022/9/27 11:35
* @description
*/
public class HungrySingLeton implements Serializable {
// 创建HungrySingLeton 的一个对象
private final static HungrySingLeton instance = new HungrySingLeton();
// 让构造函数为private,这样该类就不会被实例化
private HungrySingLeton() {
// System.out.println("饿汉式单例初始化!");
// 防止防止反射破坏单例
synchronized (HungrySingLeton.class) {
if(instance != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
}
// 获取唯一可用的对象
public static HungrySingLeton getInstance(){
return instance;
}
private Object readResolve() {
return instance;
}
/**
* 获取对象的内存地址
* @return
*/
public long getRamAddress() {
//return VM.current().addressOf(this);
return this.hashCode();
}
}
由于我们的 readResolve 方法直接返回了 instance,不会创建一个新对象,这样最终就保证了类实例对象的唯一性。
6.总结
饿汉式单例这种方式简单,也比较常用,在类创建的同时已经创建好一个静态的对象供系统使用,执行效率高。但这种方式下,因为还未调用对象就已经创建,造成资源的浪费,容易产生垃圾对象。