单例模式
在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
优势
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
枚举实现单例 — 饿汉式
开门见山, 直接上代码
public enum Singleton {
// 枚举类的一个实例
INSTANCE;
private String name;
public String getName() {
return name;
}
}
获取对象
Singleton s = Singleton.INSTANCE;
分析:
这种方法在功能上与公有域方法相似,但更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型经常成为实现 Singleton的最佳方法。注意,如果Singleton必须扩展一个超类,而不是扩展Enum的时候,则不宜使用这个方法(虽然可以声明枚举去实现接口)。
可能有些人还是迷茫, 解释下枚举
我们再项目中当需要把某些东西一一列举出来时通常会使用枚举, 如订单的各种状态, 异常的各种类型, 方便全局使用.
那么在Java中它是什么呢? 我们通过使用javap -c 看看上面的枚举类
我们可以看出在变成字节码之后,变成了类,或者说它本身就是类。继承自Enum类, 我们写的INSTANCE前面加上了static和final, 并且已new实例化, 事实上JVM帮我们写了这么多。 你写的INSTANCE就是枚举类型的一个实例。
此外,编译器会自动帮我们加一个静态方法values(). 发布我们遍历
使用静态代码块或静态变量实现单例模式 — 饿汉式
核心步骤
- 私有构造器
- 成员变量创建对象
- 提供对外获取对象方法
静态变量实现单例模式
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){
System.out.println("饿汉式单例: 对象创建成功了.......");
}
public static Singleton getInstance() {
return singleton;
}
}
静态代码块实现单例模式
对象的创建是在静态代码块中,也是对着类的加载而创建。所以和上面方法基本上一样,当然该方式也存在内存浪费问题。
public class Singleton {
private static Singleton singleton;
static {
singleton = new Singleton();
}
private Singleton(){}
public static Singleton getInstance() {
return singleton;
}
}
静态内部类方式 — 懒汉式 ( 最佳方法 )
public class Singleton {
private Singleton(){
System.out.println("懒汉式单例模式 静态内部类: 创建成功");
}
/**
* 静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的,
* @date: 2023/1/9 16:58
*/
private static class SingletonHolder{
private static final Singleton SINGLETON = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.SINGLETON;
}
}
说明
- 静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。
- 第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder, 并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
- JVM对于类的加载会加类锁,所以多线程情况下也只会保证实例化阶段是一个线程在进行。所以指令重排序就无关紧要了。同时static修饰的资源保证了全局唯一
双重检查锁方式 — 懒汉式 ( 复杂 )
public class Singleton {
private Singleton() {
System.out.println("懒汉式 单例模式: 创建成功对象...");
}
// 防止 由于jvm指令重排导致空指针异常 (百万并发底概率出现)
// 通过使用 volatile 保证了指令的可见性和有序性
private static volatile Singleton singleton;
/**
* 提供对外获取对象方法
* 通过 synchronized 添加同步锁
* @date: 2023/1/9 11:31
*/
// public static synchronized Singleton getInstance() {
// if (singleton == null) {
// singleton = new Singleton();
// }
// return singleton;
// }
/**
* 使用双重锁 提升效率
* @date: 2023/1/9 12:11
*/
public static Singleton getInstance() {
// 第一次判断 在多线程情况下 会有多个通过第一次判断
if(singleton == null) {
synchronized (Singleton.class) {
// 第二次 只允许第一次通过里面的其中一个 去执行创建
if(singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
说明
由于该方法需要较全面考虑线程安全问题和效率问题, 需要多个关键地方加锁去决绝
单例居然可以强行破解 ( 第一种枚举方式除外 )
1. 使用反射强制调用私有构造器
@Test
public void test2() throws Exception {
// 1. 获取字节码对象
Class clazz = Singleton.class;
// 2. 获取 Singleton 类的私有构造器
Constructor constructor = clazz.getDeclaredConstructor();
// 3. 取消检查访问
constructor.setAccessible(true);
// 4. 创建对象
Singleton o = (Singleton) constructor.newInstance();
System.out.println(o);
System.out.println(constructor.newInstance());
System.out.println(constructor.newInstance());
// 结果对象地址不一样
}
简单防止方案 在调用构造器方法时判断一下对象是否已经实例化了
private Singleton() {
if (singleton != null) {
throw new RuntimeException("单例对象禁止多次创建");
}
}
2. 序列化、反序列方式破坏单例模式
public void writeObject2File() throws IOException {
//1 获取对象实例
Singleton instance = Singleton.getInstance();
//2 获取输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\XiongDa\\Desktop\\a.txt"));
//3 写入对象
oos.writeObject(instance);
//4 关闭
oos.close();
}
public Singleton readObjectFromFile() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\XiongDa\\Desktop\\a.txt"));
Singleton singleton = (Singleton) ois.readObject();
ois.close();
return singleton;
}
@Test
public void test() throws Exception {
// 写入对象
writeObject2File();
// 读取
System.out.println(readObjectFromFile());
System.out.println(readObjectFromFile());
// 结果对象地址不一样
}
防止方案 在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){
System.out.println("饿汉式单例: 对象创建成功了.......");
}
public static Singleton getInstance() {
return singleton;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
public Object readResolve() {
return SingletonHolder.SINGLETON;
}
}