摸鱼设计模式——单例模式

2021-01-06 15:04:40 浏览数 (1)

单例模式的重点

  1. 私有化构造器
  2. 保证线程安全
  3. 延迟加载
  4. 防止序列化和反序列化破坏单例
  5. 防御反射攻击单例

饿汉式单例

饿汉式单例是指在单例类首次加载时就创建实例。

代码语言:javascript复制
/**
 * @Description TODO
 * @Author Sitr
 * @Date 2021/1/1 22:43
 * @Version 1.0
 **/


public class HunGry {
	//在类加载时执行该语句,创建实例。
    //final表示该实例不可修改,可以防止被使用反射机制修改。
    private static  final  HunGry hungrySingleton;
    static{
    	hungrySingleton = new HunGry();
    }
    //构造方法私有。这样外界就无法使用该构建方法,而只能使用上面所创建的类。
    private HunGry(){}
    //提供全局访问点,每当外界使用该类时,通过该方法使用。
    public static  HunGry getInstance(){
        //返回实例
        return hungrySingleton;
    }
}

饿汉式单例,无论是否使用,都直接初始化。其缺点则是会浪费内存空间。因为假如整个实例都没有被使用,那么这个类依然会创建,这就白创建了。

于是,有了第二种单例方法:

懒汉式单例

懒汉式单例是在被外部调用时才会创建的单例。

代码语言:javascript复制
/**
 * @Description TODO
 * @Author Sitr
 * @Date 2021/1/1 22:53
 * @Version 1.0
 **/


public class LazySingle {

    private static LazySingle lazySingle = null;

    private LazySingle(){}

    public static LazySingle getInstance(){
    //若lazySingle为空,即创建该实例,否则直接调用已有对象。
        if(lazySingle == null){
            lazySingle = new LazySingle();
        }
        return lazySingle;
    }

}

虽然懒汉式单例解决了内存空间浪费的问题,但是却带来了一个线程不安全的问题。

何以见得?举个简单的例子。 ExectorThread.java

代码语言:javascript复制
/**
 * @Description TODO
 * @Author Sitr
 * @Date 2021/1/1 23:00
 * @Version 1.0
 **/


public class ExectorThread implements Runnable {

    public void run(){
        LazySingle single = LazySingle.getInstance();
        System.out.println(Thread.currentThread().getName()   ":"   single);
    }
}

LazyTest.java

代码语言:javascript复制
/**
 * @Description TODO
 * @Author Sitr
 * @Date 2021/1/1 23:02
 * @Version 1.0
 **/


public class LazyTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());

        t1.start();
        t2.start();

        System.out.println("Exctor End");
    }
}

运行:

代码语言:javascript复制
Exctor End
Thread-0:com.edu.pattern.singleton.lazy.LazySingle@994e424
Thread-1:com.edu.pattern.singleton.lazy.LazySingle@781d0f57

可见,它会创建两个类,从而出现线程不安全的问题。

但是可以在getInstance方法上加一个synchronize来加锁。尽管synchronize自jdk1.6 之后性能优化了不少,但是仍然不可避免地在性能上存在一定问题。

所以,我们可以试着把synchronize判断里面,进行双重判断,也即双重检查锁。

代码语言:javascript复制
/**
 * @Description TODO
 * @Author Sitr
 * @Date 2021/1/1 23:14
 * @Version 1.0
 **/

public class LazyDoubleCheck {
    private volatile static LazyDoubleCheck lazySingle = null;

    private LazyDoubleCheck(){}

    public static LazyDoubleCheck getInstance(){
        if(lazySingle == null){
            synchronized (LazyDoubleCheck.class){//把synchronize放到里面
                if (lazySingle ==null){//再加一个判断
                    lazySingle = new LazyDoubleCheck();

                }
            }
        }
        return lazySingle;
    }
}

聊聊指令重排序

众所周知,创建类的时候,是会转换成JVM指令执行的,以下为其执行顺序:

  1. 分配内存给这个对象
  2. 初始化对象
  3. 将初始化后的对象和内存地址建立关联,并复制
  4. 用户初次访问

而由于CPU执行是抢占式的,因此第2第3个容易混淆。有时候可能先执行3,然后再执行2。多线程租的时候,容易发生此问题。而要解决此问题,则常用一个指令 volatile。

我们还有没有更好地懒汉式单例方式?

内部类。

静态内部类单例

代码语言:javascript复制
/**
 * @Description TODO
 * @Author Sitr
 * @Date 2021/1/1 23:36
 * @Version 1.0
 **/


public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton(){}

    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.lazy;
    }

    private static class LazyHolder{
        private static final LazyInnerClassSingleton lazy = new LazyInnerClassSingleton();
    }

}

使用静态内部类,前程没有使用synchronize,所以性能会好上一些。LayzHolder里面的逻辑需要等到外部调用时才会执行,巧妙地利用了内部类的特性。利用JVM底层执行逻辑,完美避免了线程安全问题。可以说是性能最优的写法。

虽然说,此时线程安全问题已经解决了,构造方法尽管是私有的,但是有可能被使用反射。 因此,我们可以在私有的构造方法改造为:

代码语言:javascript复制
    private LazyInnerClassSingleton(){
        if(LazyHolder.lazy != null){
            throw new RuntimeException("不允许构建多个实例");
        }
    }

这个就可以解决由于反射而破坏单例的情况。

但是,尽管如此,仍然有情况可以破坏该单例模式。 便是 序列化。 假如,现在我们有一个实现了序列化接口的类。 SeriableSingleton.java

代码语言:javascript复制
/**
 * @Description 实现了序列化接口的懒汉式单例
 * @Author Sitr
 * @Date 2021/1/2 0:12
 * @Version 1.0
 **/


public class SeriableSingleton implements Serializable {
    /**
     * @Author Sitr
     * @Description
     * 序列化就是把内存中的状态通过转换,转换成字节码的形式
     * 从而转换成一个IO流,写入到其他地方(可以是磁盘、网络IO) 内存中状态给永久保存下来。
     *
     * 而反序列化
     * 将已经持久化的字节码内容,转换为IO流
     * 通过IO流的读取,进而将读取的内容转化为Java对象
     * 在转换过程中会重新创建对象
     * @Date 0:13 2021/1/2
     * @Param
     * @return
     **/

    public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){return INSTANCE;}

}

用于测试的类: SeriableSingletonTest.java

代码语言:javascript复制
/**
 * @Description 测试序列化破坏单例
 * @Author Sitr
 * @Date 2021/1/2 0:11
 * @Version 1.0
 **/


public class SeriableSingletonTest {
    public static void main(String[] args) {
        SeriableSingleton s1;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;

        try {
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();


            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);
        } catch (Exception e) {
            e.printStackTrace();
        }


    }
}

执行结果为:

代码语言:javascript复制
com.edu.pattern.singleton.seriable.SeriableSingleton@5a10411
com.edu.pattern.singleton.seriable.SeriableSingleton@4eec7777
false

显然,此时单例模式被破坏了。 那么我们该如何解决呢? 我们可以从SeriableSingleton.java里面加入以下代码,重写readResolve方法

代码语言:javascript复制
    private Object readResolve(){
        return INSTANCE;
    }

现在的SeriableSingleton.java如下:

代码语言:javascript复制
public class SeriableSingleton implements Serializable {

    public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){return INSTANCE;}

    private Object readResolve(){
        return INSTANCE;
    }
}

即可解决。

那么,问题来了,为什么重写了readResolve()就可以解决呢?我们可以来看一下源码。 在SeriableSingletonTest.java中,我们是通过

代码语言:javascript复制
            s1 = (SeriableSingleton)ois.readObject();

来赋予s1对象的。先是readObject(),然后通过SeriableSingleton进行强转。先看一下readObject。

= = 我发现不同的版本的jdk其实对于序列化的源码是 不一样的。这里我按照我自己电脑上的版本为准

代码语言:javascript复制
C:Program FilesJavajdk1.8.0_261bin>java -version

java version "1.8.0_261"

Java(TM) SE Runtime Environment (build 1.8.0_261-b12)

Java HotSpot(TM) 64-Bit Server VM (build 25.261-b12, mixed mode)

我们可以看到readObject类的实现为:

代码语言:javascript复制
    public final Object readObject()
        throws IOException, ClassNotFoundException {
        return readObject(Object.class);
    }

    /**
     * Reads a String and only a string.
     *
     * @return  the String read
     * @throws  EOFException If end of file is reached.
     * @throws  IOException If other I/O error has occurred.
     */

继续往下走,见readObject(Object.class)

代码语言:javascript复制
private final Object readObject(Class<?> type)
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        if (! (type == Object.class || type == String.class))
            throw new AssertionError("internal error");

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(type, false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

在这里,我们可以看到返回的Object.class来自

代码语言:javascript复制
Object obj = readObject0(type, false);

这行代码,也即readObject0。我们继续查看readObject0内部源码。 我们重点看下面这段。

代码语言:javascript复制
case TC_OBJECT:
   if (type == String.class) {
       throw new ClassCastException("Cannot cast an object to java.lang.String");
   }
   return checkResolve(readOrdinaryObject(unshared));

这里有个checkResolve查询解析,而指向了readOrdinaryObject 读取二进制对象。我们继续往readOrdinaryObject往里看。 然后,我们就可以发现在这个方法里面的一段:

代码语言:javascript复制
Object obj;
  try {
      obj = desc.isInstantiable() ? desc.newInstance() : null;
  } catch (Exception ex) {
      throw (IOException) new InvalidClassException(
          desc.forClass().getName(),
          "unable to create instance").initCause(ex);
  }

在这里,isInstantiable() 用于检测类是否可以被初始化。如果可以被初始化,就使用newInstance()构建一个对象。如果不可以被初始化,就返回null。 isInstantiable()是如何检测的呢?

代码语言:javascript复制
boolean isInstantiable() {
    requireInitialized();
    return (cons != null);
}

这里是通过cons表示构造方法,如果构造方法为空,返回false,不会进行初始化。显然,我们前面的SeriableSingleton是有构造方法的。(尽管是私有,但这对jvm来说这就跟脱裤子放屁一样,没啥关系)所以这里的cons就不会为空,也就是说 会返回一个true,从而调用desc.newInstance()来创建对象。

因此,就会破坏单例模式。

那么如何去解决这个问题呢? 我们回到readOrdinaryObject这个方法里面,可以发现,在执行newInstance之后,会继续执行以下代码:

代码语言:javascript复制
if (obj != null &&
    handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())
{
    Object rep = desc.invokeReadResolve(obj);
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        // Filter the replacement object
        if (rep != null) {
            if (rep.getClass().isArray()) {
                filterCheck(rep.getClass(), Array.getLength(rep));
            } else {
                filterCheck(rep.getClass(), -1);
            }
        }
        handles.setObject(passHandle, obj = rep);
    }
}

在这里会调用hasReadResolveMethod()查询是否有ReadResolve方法。如果有,则会执行desc.invokeReadResolve(obj)。如果没有,就不调用了,直接使用之前已经初始化的代码。

当然,可能会有同学好奇,hasReadResolveMethod()这个方法是咋样去检测有没有ReadResolve方法的? 我们点开hasReadResolveMethod()

代码语言:javascript复制
boolean hasReadResolveMethod() {
    requireInitialized();
    return (readResolveMethod != null);
}

通过readResolveMethod是否为null来判断,而readResolveMethod的值则来源于同一个类里面的:

代码语言:javascript复制
private Method readResolveMethod;

/** local class descriptor for represented class (may point to self) */
代码语言:javascript复制
readResolveMethod = getInheritableMethod(
    cl, "readResolve", null, Object.class);

通过反射来查找是否存在readResolve方法,存在则会返回true。 只要在这个变量readResolveMethod为true,就会执行invokeReadResolve方法,使用该方法得到的值来代替。 而invokeReadResolve方法:

代码语言:javascript复制
Object invokeReadResolve(Object obj)
    throws IOException, UnsupportedOperationException
{
    requireInitialized();
    if (readResolveMethod != null) {
        try {
            return readResolveMethod.invoke(obj, (Object[]) null);
        } catch (InvocationTargetException ex) {
            Throwable th = ex.getTargetException();
            if (th instanceof ObjectStreamException) {
                throw (ObjectStreamException) th;
            } else {
                throwMiscException(th);
                throw new InternalError(th);  // never reached
            }
        } catch (IllegalAccessException ex) {
            // should not occur, as access checks have been suppressed
            throw new InternalError(ex);
        }
    } else {
        throw new UnsupportedOperationException();
    }
}

然后就好理解了,在这个方法里面通过invoke来使用已经实现的ReadResolve方法。 即:

代码语言:javascript复制
return readResolveMethod.invoke(obj, (Object[]) null);

所以,可知,其实即使你重写了ReadResolve方法,但实际上还是创建了类两次,只不过是后来创建的覆盖了之前创建的对象。而之前反序列化出来的对象则会被GC回收。

你以为自己有多厉害,其实JVM早已看透了一切。

最好的单例其实,还是注册式单例。

注册式单例

注册式单例,是指将每一个实例都缓存到统一的容器中,使用唯一标识符获取实例。

枚举式单例

代码语言:javascript复制
/**
 * @Description 枚举式单例
 * @Author Sitr
 * @Date 2021/1/2 1:30
 * @Version 1.0
 **/


public enum  EnumSingleton {
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

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

    //公共访问点
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

枚举式单例很好地解决了反序列化的时候破坏单例的问题。 (实际上,枚举式单例是编译为饿汉式单例的) 那么问题又双来了,为啥枚举式单例就可以解决反序列化的问题呢? 继续点那个readObject,进入ObjectInputStream.java。这个我们就可以继续刚才反序列化的那个类ObjectInputStream.java里面提到的

代码语言:javascript复制
case TC_ENUM:
    if (type == String.class) {
        throw new ClassCastException("Cannot cast an enum to java.lang.String");
    }
    return checkResolve(readEnum(unshared));

查看readEnum方法。可以看到是通过Enum.valueOf来确定枚举对象。

代码语言:javascript复制
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);
    }
}

可以注意到正是这一行仅枚举一次。因为这里生成的枚举都是通过类名和枚举的名字来确定枚举值。通过这两个值来注册枚举值。既然值是确定的,单一的,那么生成的对象也必然是单例的。

代码语言:javascript复制
Enum<?> en = Enum.valueOf((Class)cl, name);

但是,它到底是咋样通过枚举不会被反序列化破坏单例的呢?

我们继续。 其实是由于在jdk层面就帮我们保证了不会通过反射、序列化来创建枚举。 打开反射的类java.lang.reflect.Constructor.java,在里面的newInstance方法里,有这么一行:

代码语言:javascript复制
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    throw new IllegalArgumentException("Cannot reflectively create enum objects");

这行意思很明显,如果发现Modifier的枚举不为0,也即是一个枚举的类型,那么就抛出一个异常IllegalArgumentException("Cannot reflectively create enum objects")。

容器式单例

Spring所采用的单例模式。

代码语言:javascript复制
/**
 * @Description 容器式单例
 * @Author Sitr
 * @Date 2021/1/2 13:13
 * @Version 1.0
 **/


public class ContainerSingleton {
    private ContainerSingleton(){}

    private static Map<String,Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(String className){
        synchronized (ioc){
            if(!ioc.containsKey(className)){
                Object obj = null;
                try{
                    obj = Class.forName(className).newInstance();//采用简单工厂模式。
                    ioc.put(className,obj);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return obj;
            }
            return ioc.get(className);
        }

    }
}

容器式单例最大的优点就是对象方便管理,是懒加载的一种,如果不加上synchronize会存在线程安全问题。而加上synchronize之后,解决线程安全问题,但是会影响性能。而Spring正是采用这种方式实现单例模式。

ThreadLocal的单例模式

线程内部的线程安全的单例模式,保证线程内部的全局唯一,天生线程安全。但是在切换线程之后就不再是线程安全了,是个伪线程安全。

代码语言:javascript复制
/**
 * @Description ThreadLocal实现的单例模式
 * @Author Sitr
 * @Date 2021/1/2 13:35
 * @Version 1.0
 **/


public class ThreadLocalSingleton {

    private ThreadLocalSingleton(){}

    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };
    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}

ThreadLocal单例模式是如何保证线程内全局唯一的? 我们查看java.lang.ThreadLocal.java的源码。

代码语言:javascript复制
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

我们可以看到 这里的get是通过ThreadLocalMap来获取的,我们继续查看ThreadLocalMap的get部分。

代码语言:javascript复制
  public void set(T value) {
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
  }

可以看到是使用map.set来设置的,通过this和value来注册单例对象。因此这也是一种注册类的单例。 这种单例模式常用于ORM框架配置数据源,实现ThreadLocal来实现多数据源动态切换。

单例模式的优点

单例模式在内存中只有一个实例,可以有效减少内存的开销,避免对资源的多重占用。同时设置了全局访问点,可以严格控制访问。

单例模式的缺点

缺点也很明显,不符合开闭原则。没有接口,扩展起来会比较困难。如果要扩展单例模式,只能通过修改代码来扩展。

0 人点赞