轻松搞定单例模式以及线程安全等问题

单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

  • 定义:保证一个类仅有一个实例,并提供一个全局访问点
  • 类型:创建型
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

适用场景

  • 想确保任何情况下都绝对只有一个实例

优点与缺点

优点:

  • 在内存里只有一个实例,减少内存开销
  • 可以避免对资源的多重占用
  • 设置全局访问点,严格控制访问

缺点:

  • 没有接口,拓展困难

单例模式的重点

  • 私有构造函数,因为单例模式只提供一个全局访问点,不允许将构造函数暴露给用户
  • 在多线程下,可能发生的线程安全问题
  • 是否延迟加载,即大家熟知的饿汉式懒汉式
  • 序列化以及反序列化的安全问题
  • 防止使用反射破坏单例

编码实现单例模式—懒汉式

public class LazySingleton {
    private static LazySingleton lazySingleton = null;

    private LazySingleton() {
    }

    public synchronized static LazySingleton getInstance() {
        //延迟加载
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

//    与上面的写法是一致的
//    public  static LazySingleton getInstance() {
//        synchronized (LazySingleton.class) {
//            if (lazySingleton == null) {
//                lazySingleton = new LazySingleton();
//            }
//        }
//        //延迟加载
//        return lazySingleton;
//    }
}

在多线程下,为了避免多次创建实例,破坏单例。所以可以使用synchronized来修饰方法,达到同步的目的。对于静态方法来说,锁住的是类的Class对象,即其他线程无法再访问该类,Class对象的加锁与解锁需要消耗资源,且这段代码需要同步的只是下面代码。可以对这种方式进行改进。

if (lazySingleton == null) {
    lazySingleton = new LazySingleton();
}

双重检测懒汉式

public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton lazySingleton = null;

    private LazyDoubleCheckSingleton() {
    }

    public static LazyDoubleCheckSingleton getInstance() {
        //延迟加载
        if (lazySingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazySingleton;
    }
}

使用同步代码块,避免了只要访问getInstance方法就锁住Class对象。双重判断也可以避免因为线程调度而产生的实例多次创建的问题。但这样也没完全避免线程安全问题,就是指令重排序带来的问题。这里不讨论指令重排序以及JMM和多线程相关的原理。
new LazyDoubleCheckSingleton()其实包括了3个步骤:

  1. 分配对象的内存空间
  2. 初始化对象
  3. 设置instance指向内存可见(引用)

2和3是可以调换顺序的,由于2、3之间没有依赖关系,cpu可能会对指令重排序达到提高效率的目的。

image.png
所以可能出上图发生的情况,由于重排序,instance对象并没有完成初始化,但实际上此时instance对象已经不是null。把未初始化的对象发布了出去,这就产生了安全问题!
这里主要讲2种解决方法:禁用指令重排序以及对其他线程指令重排序不可见。

volatile禁用重排序
public class LazyDoubleCheckSingleton {
    //禁用重排序
    private volatile static LazyDoubleCheckSingleton lazySingleton = null;

    private LazyDoubleCheckSingleton() {
    }

    /**
     *由于直接在静态方法使用synchronized,锁的是class对象,即其他线程无法获取此锁。降低性能可以优化。
     *正真需要同步的是延迟加载这个代码块,而不是整个方法。
     * 由于可能发生重排序导致对象未初始化就先完成引用的操作—> lazySingleton !=null,会出现线程安全问题。
     * 所以使用volatile使lazySingleton对所有线程可见。
     */
    public static LazyDoubleCheckSingleton getInstance() {
        //延迟加载
        if (lazySingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new LazyDoubleCheckSingleton();
                    // TODO: 2020/7/4 理解volatile JMM 重排序 happens-before
                    /*
                       1. 分配内存
                       2. 初始化对象
                       3. 将lazySingleton 指向该对象
                       可能会发生指令重排序(仅针对单个处理器中执行的指令序列和单个线程中执行的操作)
                     */
                }
            }
        }
        return lazySingleton;
    }
}

volatile禁用重排序的原理是内存屏障,这里不作讨论。

基于静态内部类的懒加载
public class StaticInnerClassSingleton {
    private  static class InnerClass{
        private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }

    private StaticInnerClassSingleton() {

    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.instance;
    }
}

image.png

首先对于内部类,static final修饰的常量,在类的初始化阶段就完成赋值了。并且类的初始化具有Class对象锁,只有一个线程可获取到Class对象锁。所以就算发生了重排序对于其他线程也是不可见的。从而屏蔽了重排序问题。

编码实现单例模式—饿汉式

public class HungrySingleton implements Serializable {
    public static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

    private HungrySingleton() {
        if (HUNGRY_SINGLETON!=null) {
            throw new RuntimeException("禁止在单例模式使用反射");
        }
    }

    public static HungrySingleton getInstance() {
        return HUNGRY_SINGLETON;
    }

    private Object readResolve() {
        return HUNGRY_SINGLETON;
    }
}

饿汉式是比较容易实现的,且没有线程安全问题。因为instance为常量,在类加载是就已经赋值了。
上面例子种增加了一些无关代码,下面再来解释一下原因,主要是针对反射破坏以及序列化破坏单例方面。

破坏单例模式的一些问题

主要包括:

  • 在多线程下,可能发生的线程安全问题
  • 序列化以及反序列化的安全问题
  • 防止使用反射破坏单例

线程安全问题已经在上面说明了,就不赘述。

防止使用反射破坏单例

Class<HungrySingleton> objectClass = HungrySingleton.class;
Constructor<HungrySingleton> constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton newInstance = constructor.newInstance();

我们可以通过反射来调用单例类的私有构造器,从而破坏了单例模式。

 private HungrySingleton() {
        if (HUNGRY_SINGLETON!=null) {
            throw new RuntimeException("禁止在单例模式使用反射");
        }
    }

这个时候,这段的代码的作用就看出来,由于在类加载阶段就已经生成了instance对象,我们在构造器对instance进行判断,抛出异常。
可以有效防止外部通过反射调用私有构造函数。但对于懒汉式,就很难防止反射攻击,因为懒汉式的instance对象是在getInstance方法中生成的,所以无论采取什么方式,比如:标志位(flag),反射都可以获取到变量,修改变量来达到new一个新的instance的目的。

序列化以及反序列化的安全问题

  HungrySingleton instance = HungrySingleton.getInstance();
  ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hungry_singleton"));
  oos.writeObject(instance);
  ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hungry_singleton"));
  HungrySingleton newInstance = (HungrySingleton) ois.readObject();
  System.out.println(instance.getData() == newInstance.getData());

image.png
通过结果,输入输出的对象不是同一个对象。那么为什么两次的结果不一致,这里只展出jdk的关键源码。

/* 定义在ObjectInputSteam的readOrdinaryObject方法
该类是否包含有构造器,有则返回true,当返回true时,通过反射new一个instance
*/
obj = desc.isInstantiable() ? desc.newInstance() : null;
boolean isInstantiable() {
        requireInitialized();
        return (cons != null);
}

/*
当类里包含有readResolve方法,就会调用readResolve的返回值作为序列化的返回值,不然将返回返回上面新new的对象
*/
if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
        }
boolean hasReadResolveMethod() {
        requireInitialized();
        return (readResolveMethod != null);
    }
 readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class);

主要的逻辑就是先判断该类是不是可以支持序列化,然后先通过反射new一个instance,之后再判断类里是否有readResolve方法,有则返回readResolve方法返回的object,没有则返回新new出的object。所以在单例类里补充readResolve方法就可以解决序列化的问题。

 private Object readResolve() {
        return HUNGRY_SINGLETON;
    }

image.png

单例模式最佳实践—基于枚举类

public enum  EnumInstance {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumInstance getInstance() {
        return INSTANCE;
    }
}

我们使用jad来反编译这个类:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumInstance.java

package com.qzh.design.pattern.creational.singleton;


public final class EnumInstance extends Enum
{

    public static EnumInstance[] values()
    {
        return (EnumInstance[])$VALUES.clone();
    }

    public static EnumInstance valueOf(String name)
    {
        return (EnumInstance)Enum.valueOf(com/qzh/design/pattern/creational/singleton/EnumInstance, name);
    }

    private EnumInstance(String s, int i)
    {
        super(s, i);
    }

    public Object getData()
    {
        return data;
    }

    public void setData(Object data)
    {
        this.data = data;
    }

    public static EnumInstance getInstance()
    {
        return INSTANCE;
    }

    public static final EnumInstance INSTANCE;
    private Object data;
    private static final EnumInstance $VALUES[];

    static 
    {
        INSTANCE = new EnumInstance("INSTANCE", 0);
        $VALUES = (new EnumInstance[] {
            INSTANCE
        });
    }
}

可以看到INSTANCE在类加载阶段就初始化了,所以是线程安全的。且具有一个私有的构造函数,符合单例模式。
并且枚举类是不支持反射的。在Constructor的newInstance方法中

if ((this.clazz.getModifiers() & 16384) != 0) {
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        }

可以看到无法使用构造器来反射出枚举类对象。
枚举类也天生支持序列化,我们继续来看ObjectInputSteam的代码:

String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
  try {
         @SuppressWarnings("unchecked")
         Enum<?> en = Enum.valueOf((Class)cl, name);
         result = en;
 } catch (IllegalArgumentException ex) {
        throw (IOException) new InvalidObjectException(
         "enum constant " + name + " does not exist in " +
       cl).initCause(ex);
   }
 if (!unshared) {
        handles.setObject(enumHandle, result);
     }
 }

可以看到,是通过名称去获取枚举对线,而枚举对象又是static final修饰的,所以拿到的都是同一个对象,有兴趣的朋友可以深入到Eunm的valueOf方法去分析。

后记

总结了常见的单例模式的一些具体实现,以及一步步去优化代码,解决问题。以及解释大家平时不常用的用枚举类来实现单例。希望大家有所收获!
github地址
源码:https://github.com/DiangD/design_pattern_practice

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值