【单例模式】:一个类在Java虚拟机中只有一个对象,并提供一个全局访问点,使对象具有了唯一性
举例:数据库连接对象、线程池、缓存、日志对象
创建方式:懒汉式、饿汉式

【饿汉式】

class HungrySingleton{
    private static HungrySingleton hungry = new HungrySingleton();
    private HungrySingleton(){};
    public static HungrySingleton getInstance(){
        return hungry;
    }
}

饿汉式将构造器私有化,避免类在外部被实例化,外部只能通过get方法获取唯一实例。由于在类加载时就创建单例对象,属于天然的线程安全。

【懒汉式】
普通的懒汉式是线程不安全的,如下:

class LazySingleton{
    private static LazySingleton instance;
    private LazySingleton(){};
    public static LazySingleton getInstance(){
        if(instance==null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

如果多个线程同时获取单例getInstance,可能创建出多个单例对象:
在这里插入图片描述
方案1. 在getInstance方法上加锁

public static synchronized LazySingleton getInstance(){
        if(instance==null) {
            instance = new LazySingleton();
        }
        return instance;
    }

每次获取对象都要上锁,性能较差,不推荐

方案2. 双检查锁DCL

class Singleton {
    private static volatile Singleton singleton;
    private Singleton(){};
    public static Singleton getInstance(){
        if(singleton==null){
            synchronized (Singleton.class){
                if(singleton==null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

  这样做的好处是一开始判断单例是否存在,避免了每次上锁,不存在则只在第一次上锁。

注意,这里使用volatile修饰单例对象是为了避免操作系统在进行new Singleton()这个非原子操作时指令重排序,具体步骤是:
1.分配内存空间
2.执行构造方法,初始化对象
3.将对象指向该内存空间
重排序1-2-3–》1-3-2
此时若线程2执行getInstance发现单例对象已有指针(不为null),但实际上内存空间可能还未存放对象,导致线程2后序操作报错。

方案3. 静态内部类

class SingletonInterClass{
    private SingletonInterClass(){};

    private static class InnerClass{
        private static final SingletonInterClass SINGLETON = new SingletonInterClass();
    }
    public static final SingletonInterClass getInstance(){
        return InnerClass.SINGLETON;
    }
}

  事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。
在这里插入图片描述  那么有没有什么措施呢?

	private Singleton(){
        synchronized (Singleton.class){
            if(singleton!=null){
                throw new RuntimeException("不要用反射破坏单例");
            }
        }
    }

我们在构造器里加一个检测+抛出异常,就能阻止反射创建对象,最后是三重检测。

问题又来了,如果一开始没有创建单例,两个对象都用反射创建,一样可以破坏啊。别急,我们换一种检测:

	private static boolean qinjiang = false;
    private Singleton(){
        synchronized (Singleton.class){
            if(!qinjiang){
                qinjiang = true;
            }else{
                throw new RuntimeException("不要用反射破坏单例");
            }
        }
    }

可是,加入通过反编译获取到了标志变量,并通过反射机制修改,还是可以破解的
在这里插入图片描述
魔高一尺,道高一丈
枚举类型可以防止反射破坏单例

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }

    private EnumSingle() {
    }
}
	public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        EnumSingle instance2 = EnumSingle.INSTANCE;
        //反射
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        EnumSingle instance3 = constructor.newInstance();

        System.out.println(instance1 == instance2);
        System.out.println(instance1 == instance3);
    }

第一个比较结果为true,第二个方法抛出异常,但这并不是我们想要看到的异常信息,而是下面的绿字部分
在这里插入图片描述
在这里插入图片描述
通过jad反编译可以看出实际被调用的是一个有参构造器,重新获取构造器

Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingle instance3 = constructor.newInstance();

再次运行,异常如下:
在这里插入图片描述
不能通过反射创建枚举类,这就是

以上就是我总结的关于饿汉式和懒汉式的常见方法,欢迎大神补充…

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐