Java创建对象的最佳方式:单例模式(Singleton)

本文详细介绍了Java中的单例模式,包括饿汉式(静态变量和静态代码块)的实现、懒汉式的改进(双重检查锁和volatile关键字),以及如何通过枚举和防止反射、反序列化来确保单例模式的正确性。
摘要由CSDN通过智能技术生成

前言

单例模式是java中最简单的设计模式之一,属于创建式模式,提供了一种创建对象的最佳方式。

具体而言,单例模式涉及到一个具体的类,这个类可以确保只有单个对象被创建。它包含一个访问其唯一对象的方法,供外部直接调用,而不需要创建这个类的示例。

简而言之,可以不再new一个他的实例,而是直接调用方法。

实现

单例模式分为两种:

  • 饿汉式:类加载时就会导致该单例对象被创建
  • 懒汉式:类加载不会导致该单例对象被创建,而是首次使用时才会

饿汉式

静态变量方式

public class Singleton {
    // 私有构造方法
    private Singleton() {}
    
    // 静态方式创建该类的对象
    private static Singleton instance = new Singleton();

    // 对外提供静态方法,用于获取该对象
    public static Singleton getInstance() {
        return instance;
    }
}

将对象的实例创建为静态,保证了实例永远只有一份,且在该类加载时就会立即创建在jvm内存的方法区,程序运行期间永久存在,因此当对象太大时会造成浪费。

静态代码块方式

public class Singleton {
    // 私有构造方法
    private Singleton() {}
    
    // 静态创建该类对象
    private static Singleton instance;
    static {
        instance = new Singleton();
    }

    // 对外提供静态方法,用于获取该对象
    public static Singleton getInstance() {
        return instance;
    }
}

对象的创建在代码块中,也是随着类的加载而创建,两种方式基本相同。

 懒汉式

从名字出发,饿汉式会一直饿着,需要不断有食物,即对象一直存在。

懒汉式则是只有在饿的时候才会寻找食物,即请求对象实例。

所以,只要将饿汉式的创建对象放到getInstance方法中,即只有在调用该方法时才创建,我们就完成了懒加载的效果。

更改初始化时机

public class Singleton {
    // 私有构造方法
    private Singleton() {}
    
    // 静态创建该类对象
    private static Singleton instance;

    // 对外提供静态方法,用于获取该对象
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

但这样的代码是线程不安全的,可以设想,如果有两个线程同时调用getInstance方法,他们都发现了instance == null,那么此时就会有两个实例化的instance,这就违背了单例模式的初衷了。

 最容易想到的方法就是给getInstance方法上锁。

上锁:防止初始化多次

public class Singleton {
    // 私有构造方法
    private Singleton() {}
    
    // 静态创建该类对象
    private static Singleton instance;

    // 上锁
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

但此处依然有优化的空间,如果instance不为空,让线程调用就好了,不一定要持有锁,因此我们引申出双重检查锁模式。

优化:双重锁模式

public class Singleton {
    // 私有构造方法
    private Singleton() {}
    
    // 静态创建该类对象
    private static Singleton instance;

    // 上锁
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                // 线程拥有锁之后,再判断是否为空
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

为什么要两次判断是否为空?

例如有ab两个线程,当a先获得锁并初始化instance实例后,b获得锁,如果此时不判空,则会重复初始化,因为此时两者都进入了锁之内了,上一个判空已经过时。

但是,jvm在实例化对象的时候,会进行优化和指令重排序操作,多线程的情况下,可能会产生空指针,此时我们可以使用volatile关键字。

使用volatile关键字修饰的变量,可以保证其指令执行的顺序与我们写明的顺序一致,最终代码如下:

public class Singleton {
    // 私有构造方法
    private Singleton() {}
    
    // 静态创建该类对象
    private static volatile Singleton instance;

    // 上锁
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                // 线程拥有锁之后,再判断是否为空
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

最终进化:枚举实现

在java1.5之后,使用java实现单例模式的方式多了一种枚举。

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("这是枚举类型的单例模式!");
    }
}

我们可以对枚举方式实现的实例进行反编译,可以得到如下的代码:

public final class Singleton extends Enum {
    public static Singleton[] values() {
        return (Singleton[])$VALUES.clone();
    }
    
    public static Singleton valueOf(String name) {
        return (Singleton)Enum.valueOf(com/qgn/mianshi/main/Singleton, name);
    }
    
    private Singleton(String s, int i) {
        super(s, i);
    }
        
    public static final Singleton INSTANCE;
    
    private static final Singleton $VALUES[];
    
    static {
        INSTANCE = new Singleton("INSTANCE", 0);
        $VALUES = (new Singleton[] {
            INSTANCE
        });
    }
}

可见Singleton继承了Enum,并重写了Enum类中的一些方法,基本上这些方法都是静态的。

这种实现方式的优点在于,不需要做额外的操作去保证对象单一性与线程安全,同时使用枚举可以防止调用者使用反射、反序列化强制生成多个单例对象,破坏单例模式。

但是如果没使用枚举时,要怎么防止反射和反序列化破坏单例呢?

防止反射、反序列化破坏单例模式

防反射

反射是一种暴力获取对象实例的方法,可以直接获取private修饰的构造函数,所以在放反射上只能被动防御。

既然能访问构造函数,那我们也可以在构造函数中建立防御机制。

public class Singleton  {
    private static volatile boolean flag = false;
    //私有构造方法
    private Singleton() {
        synchronized (Singleton.class){
        if (flag){
                throw new RuntimeException();
        }
         flag = true;
    }}
    private static class SingletonHolder {

        private static final Singleton INSTANCE = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

防反序列化

在readObject()方法的调用栈的底层方法中有这么两个方法:
hasReadResolveMethod:
表示如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回true。

所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

public class Singleton implements Serializable {
    //私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    private Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}

  • 11
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值