前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >摸鱼设计模式——单例模式

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

原创
作者头像
摸鱼-Sitr
修改2021-01-06 15:04:40
6520
修改2021-01-06 15:04:40
举报
文章被收录于专栏:摸鱼的Java

单例模式的重点

  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 Files\Java\jdk1.8.0_261\bin>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来实现多数据源动态切换。

单例模式的优点

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

单例模式的缺点

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 单例模式的重点
  • 饿汉式单例
  • 懒汉式单例
    • 聊聊指令重排序
      • 静态内部类单例
      • 注册式单例
        • 枚举式单例
          • 容器式单例
            • ThreadLocal的单例模式
            • 单例模式的优点
            • 单例模式的缺点
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档