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

设计模式——单例模式

作者头像
健程之道
修改2020-03-11 11:01:56
3820
修改2020-03-11 11:01:56
举报
文章被收录于专栏:健程之道健程之道

关于单例模式,这是面试时最容易遇到的问题。当时以为很简单的内容,深挖一下,也可以关联出类加载、序列化等知识。

饿汉式

我们先来看看基本的饿汉式写法:

代码语言:javascript
复制
public class Hungry {

    private static final Hungry instance = new Hungry();

    private Hungry() {}

    public Hungry getInstance() {
        return instance;
    }
}

优点:写法简答,不需要考虑多线程等问题。

缺点:如果该实例从未被用到的话,相当于资源浪费。

static 代码块

我们也可以用 static 代码块的方式,实现饿汉式:

代码语言:javascript
复制
public class Hungry {

    private static final Hungry instance;

    static {
        instance = new Hungry();
    }

    private Hungry() {}

    public Hungry getInstance() {
        return instance;
    }
}

这就是利用了 static 代码块的功能:它是随着类的加载而执行,只执行一次,并优先于主函数。

懒汉式

我们先来看看基本的懒汉式写法:

代码语言:javascript
复制
public class Lazy {

    private static volatile Lazy instance;

    private Lazy(){}

    public static Lazy getInstance() {
        if (instance == null) {
            synchronized (Lazy.class) {
                if (instance == null) {
                    instance = new Lazy();
                }
            }
        }
        return instance;
    }
}

这里就涉及到了很多知识点,让我们一一讲解。

volatile

代码语言:javascript
复制
这里使用 volatile,主要是为了禁止指令重排序。

主要就是针对 instance = new Lazy(); 这1行命令,在 JVM 中至少对应3条指令:
1. 给 instance 分配内存空间。
2. 调用 Lazy 的构造方法等来初始化 instance。
3. 将 instance 对象指向分配的内存空间(执行完这一步,instance 就不是 null 了)。

这里需要注意,JVM 会对指令进行优化排序,就是第 2 步与第 3 步的顺序是不一定的,可能是 1-2-3 ,也可能是 1-3-2 。

如果是后者,可能1个线程执行完 1-3 之后,另一个线程进入了

以上这一段想必就是大家平常看到的解释了,原本我对此也是深信不疑的,但是因为本地一直无法复现,因此让我产生了怀疑。

查阅资料后,可能是和以下两点有关。

Intel 64/IA-32架构下的内存访问重排序

指令重排发生在处理器平台,对于Java来说是看不到的,因为Jvm基于线程栈,所有的读写都对应了 store 操作,而Intel 64/IA-32架构下处理器不需要LoadLoad、LoadStore、StoreStore屏障,因此不会发生需要这三种屏障的重排序。所以,store 操作之间是不会重排序的。

JMM

JMM 抽象地将内存分为主内存和本地内存,各个线程有各自的本地内存。

如果2个线程在执行Lazy.getInstance()方法,instance作为 static 修改的变量,处于主内存中,两个线程会各自复制instance到本地内存中,当线程1执行instance = new Lazy();方法,除非全部结束,否则不会将本地内存中的instance写回主内存中。

以上也可能是我想错了,但欢迎大家一起探讨。

double-check

为什么要有双重检查呢?

代码语言:javascript
复制
第二个 if 判定:是为了保证当有两个线程同时通过了第一个 if 判定,一个线程获取到锁,生成了 Lazy 的一个实例,然后第二个线程获取到锁,如果没有第二个 if 判断,那么此时会再次生成生成 Lazy 的一个实例。
第一个 if 判定:是为了保证多线程同时执行,如果没有第一个 if 判断,所有线程都会串行执行,效率低下。

静态内部类

也可以利用静态内部类来实现:

代码语言:javascript
复制
public class Lazy {

    private Lazy() {}

    private static class InnerLazy {
        private static final Lazy INSTANCE = new Lazy();
    }

    public static Lazy getInstance() {
        return InnerLazy.INSTANCE;
    }
}

为什么这样能实现懒加载呢?

因为只有当调用InnerLazy.INSTANCE时,才会对 InnnerLazy 类进行初始化,然后才会调用 Lazy 的构造方法,这也是由类加载机制保证的:

代码语言:javascript
复制
遇到 new 、getstatic、putstatic 或者 invokestatic 这 4 条字节码指令时,如果没有对类进行初始化,则需要先触发其初始化。
这4个指令对应的 Java 场景是:使用 new 新建一个 Java 对象,访问或者设置一个类的静态字段,访问一个类的静态方法的时候。

优缺点

以上方法的优缺点:

优点:使用的时候才会进行初始化,拥有更好的资源优化。

缺点:

  1. 除去最后一种静态内部类之外,写法都比较繁琐。
  2. 如果使用反射或者反序列化,依旧可以强制生成新的实例。

针对第2点,我们可以举例子来说明一下:

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

    public String name;

    private Lazy() {
        name = String.valueOf(System.currentTimeMillis());
    }

    private static class InnerLazy {
        private static final Lazy INSTANCE = new Lazy();
    }

    public static Lazy getInstance() {
        return InnerLazy.INSTANCE;
    }

    public void print() {
        System.out.println("Lazy print : " + name);
    }

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, IOException, ClassNotFoundException {
        Lazy instance1 = Lazy.getInstance();
        instance1.print();

        // 反射
        Lazy instance3 = Lazy.class.newInstance();
        instance3.print();
        System.out.println(instance1 == instance3);

        // 反序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file"));
        oos.writeObject(instance1);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("file"));
        Lazy instance2 = (Lazy) ois.readObject();
        instance2.print();
        System.out.println(instance1 == instance2);
    }
}

输出结果为:

代码语言:javascript
复制
Lazy print : 1583410057762
Lazy print : 1583410057768
false
Lazy print : 1583410057762
false

说明反射和反序列化,都会破坏以上写法的单例特征。那该如何解决呢?

  1. 针对反射,解决起来比较简单,可以在构造方法中判断一下 InnerLazy.INSTANCE ,如果不为 null ,则抛出异常。
  2. 针对反序列化,可以实现接口 Serializable ,重写 readResolve 方法,返回单例对象 InnerLazy.INSTANCE。

看看修改后的代码:

代码语言:javascript
复制
package singleton;

import java.io.*;

public class Lazy implements Serializable {

    public String name;

    private Lazy() {
        if (InnerLazy.INSTANCE != null) {
            throw new RuntimeException("can not be invoked");
        }
        name = String.valueOf(System.currentTimeMillis());
    }

    private static class InnerLazy {
        private static final Lazy INSTANCE = new Lazy();
    }

    public static Lazy getInstance() {
        return InnerLazy.INSTANCE;
    }

    public void print() {
        System.out.println("Lazy print : " + name);
    }

    private Object readResolve() {
        return InnerLazy.INSTANCE;
    }

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, IOException, ClassNotFoundException {
        Lazy instance1 = Lazy.getInstance();
        instance1.print();

        // 反序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file"));
        oos.writeObject(instance1);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("file"));
        Lazy instance2 = (Lazy) ois.readObject();
        instance2.print();
        System.out.println(instance1 == instance2);

        // 反射
        Lazy instance3 = Lazy.class.newInstance();
        instance3.print();
        System.out.println(instance1 == instance3);
    }
}

运行结果为:

代码语言:javascript
复制
Lazy print : 1583409803987
Lazy print : 1583409803987
true
Exception in thread "main" java.lang.RuntimeException: can not be invoked
    at singleton.Lazy.<init>(Lazy.java:11)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at java.lang.Class.newInstance(Class.java:442)
    at singleton.Lazy.main(Lazy.java:46)

枚举类

针对上面的缺点,我们也可以用 enum 解决。来看看写法:

代码语言:javascript
复制
package singleton;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public enum Singleton {

    INSTANCE;

    private String name;

    private Singleton() {
        name = String.valueOf(System.currentTimeMillis());
    }

    public void print() {
        System.out.println("Lazy print : " + name);
    }

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, IOException {
        Singleton instance1 = Singleton.INSTANCE;
        instance1.print();
        // 反序列化
        ObjectMapper objectMapper = new ObjectMapper();
        String content = objectMapper.writeValueAsString(instance1);
        Singleton instance3 = objectMapper.readValue(content, Singleton.class);
        System.out.println(instance1 == instance3);
        instance3.print();
        // 反射
        Singleton instance2 = Singleton.class.newInstance();
        System.out.println(instance1 == instance2);
        instance2.print();
    }
}

运行结果为:

代码语言:javascript
复制
Lazy print : 1583409004276
true
Lazy print : 1583409004276
Exception in thread "main" java.lang.InstantiationException: singleton.Singleton
    at java.lang.Class.newInstance(Class.java:427)
    at singleton.Singleton.main(Singleton.java:31)
Caused by: java.lang.NoSuchMethodException: singleton.Singleton.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.newInstance(Class.java:412)
    ... 1 more

首先,枚举是不能被反射生成实例的,这也就解决了反射破坏单例的问题。

其次,在序列化枚举类型时,只会存储枚举类的引用和枚举常量的名称。随后的反序列化的过程中,这些信息被用来在运行时环境中查找存在的枚举类型对象,这也就解决了序列化破坏单例的问题。

但需要注意:这种方法属于饿汉模式,所以有浪费资源的隐患,但如果你的单例对象并不占用资源,没有状态变量,那么这种方式就很适合你。

总结

以上就是我关于单例模式的一些理解,简单的问题,也可以关联出并发、类加载、序列化等重要知识。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 健程之道 微信公众号,前往查看

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

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 饿汉式
    • static 代码块
    • 懒汉式
      • volatile
        • Intel 64/IA-32架构下的内存访问重排序
        • JMM
      • double-check
        • 静态内部类
          • 优缺点
          • 枚举类
          • 总结
          相关产品与服务
          文件存储
          文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档