概述
单例模式
在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
优势
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
- 由于 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;
}
}
最后
以上就是传统冰棍为你收集整理的单例模式的最佳实践居然是使用枚举 ?单例模式使用静态代码块或静态变量实现单例模式 — 饿汉式静态内部类方式 — 懒汉式 ( 最佳方法 )双重检查锁方式 — 懒汉式 ( 复杂 )单例居然可以强行破解 ( 第一种枚举方式除外 )的全部内容,希望文章能够帮你解决单例模式的最佳实践居然是使用枚举 ?单例模式使用静态代码块或静态变量实现单例模式 — 饿汉式静态内部类方式 — 懒汉式 ( 最佳方法 )双重检查锁方式 — 懒汉式 ( 复杂 )单例居然可以强行破解 ( 第一种枚举方式除外 )所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复