关于单例模式,这是面试时最容易遇到的问题。当时以为很简单的内容,深挖一下,也可以关联出类加载、序列化等知识。
饿汉式
我们先来看看基本的饿汉式写法:
public class Hungry {
private static final Hungry instance = new Hungry();
private Hungry() {}
public Hungry getInstance() {
return instance;
}
}
优点:写法简答,不需要考虑多线程等问题。
缺点:如果该实例从未被用到的话,相当于资源浪费。
static 代码块
我们也可以用 static 代码块的方式,实现饿汉式:
public class Hungry {
private static final Hungry instance;
static {
instance = new Hungry();
}
private Hungry() {}
public Hungry getInstance() {
return instance;
}
}
这就是利用了 static 代码块的功能:它是随着类的加载而执行,只执行一次,并优先于主函数。
懒汉式
我们先来看看基本的懒汉式写法:
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
这里使用 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
为什么要有双重检查呢?
第二个 if 判定:是为了保证当有两个线程同时通过了第一个 if 判定,一个线程获取到锁,生成了 Lazy 的一个实例,然后第二个线程获取到锁,如果没有第二个 if 判断,那么此时会再次生成生成 Lazy 的一个实例。
第一个 if 判定:是为了保证多线程同时执行,如果没有第一个 if 判断,所有线程都会串行执行,效率低下。
静态内部类
也可以利用静态内部类来实现:
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 的构造方法,这也是由类加载机制
保证的:
遇到 new 、getstatic、putstatic 或者 invokestatic 这 4 条字节码指令时,如果没有对类进行初始化,则需要先触发其初始化。
这4个指令对应的 Java 场景是:使用 new 新建一个 Java 对象,访问或者设置一个类的静态字段,访问一个类的静态方法的时候。
优缺点
以上方法的优缺点:
优点:使用的时候才会进行初始化,拥有更好的资源优化。
缺点:
- 除去最后一种
静态内部类
之外,写法都比较繁琐。 - 如果使用反射或者反序列化,依旧可以强制生成新的实例。
针对第2点,我们可以举例子来说明一下:
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);
}
}
输出结果为:
Lazy print : 1583410057762
Lazy print : 1583410057768
false
Lazy print : 1583410057762
false
说明反射和反序列化,都会破坏以上写法的单例特征。那该如何解决呢?
- 针对反射,解决起来比较简单,可以在构造方法中判断一下 InnerLazy.INSTANCE ,如果不为 null ,则抛出异常。
- 针对反序列化,可以实现接口 Serializable ,重写 readResolve 方法,返回单例对象 InnerLazy.INSTANCE。
看看修改后的代码:
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);
}
}
运行结果为:
Lazy print : 1583409803987
Lazy print : 1583409803987
true
Exception in thread "main" java.lang.RuntimeException: can not be invoked
at singleton.Lazy.(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 解决。来看看写法:
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();
}
}
运行结果为:
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.()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.newInstance(Class.java:412)
... 1 more
首先,枚举是不能被反射生成实例的,这也就解决了反射破坏单例
的问题。
其次,在序列化枚举类型时,只会存储枚举类的引用和枚举常量的名称。随后的反序列化的过程中,这些信息被用来在运行时环境中查找存在的枚举类型对象,这也就解决了序列化破坏单例
的问题。
但需要注意:这种方法属于饿汉模式
,所以有浪费资源的隐患,但如果你的单例对象并不占用资源,没有状态变量,那么这种方式就很适合你。
总结
以上就是我关于单例模式的一些理解,简单的问题,也可以关联出并发、类加载、序列化等重要知识。
有兴趣的话可以访问我的博客或者关注我的公众号、头条号,说不定会有意外的惊喜。
公众号:健程之道