Java单例设计模式

Java 实现单例设计模式的部分例子,并不是全部

普通饿汉式

何为饿汉,就是很饿了,要马上能吃到东西(实例先初始化好,要用时就直接拿来用)

1
2
3
4
5
6
7
8
9
10
11
12
package singleton;

public class Singleton {
private static final Singleton INSTANCE = new Singleton();

private Singleton() {
}

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

这种方式就能实现单例了吗?测试下

1
2
3
4
public static void main(String[] args) {
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
}

输出相同的地址,确实能实现

1
2
singleton.Singleton@6bf2d08e
singleton.Singleton@6bf2d08e

从上面输出可以看到是实现了的,但是有潜在的问题

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws Exception {
System.out.println(Singleton.getInstance());

Constructor<?>[] declaredConstructors = Singleton.class.getDeclaredConstructors();
for (Constructor<?> declaredConstructor : declaredConstructors) {
declaredConstructor.setAccessible(true);
Object obj = declaredConstructor.newInstance();
System.out.println(obj);
}
}

这样就会得到两个不同的对象

1
2
singleton.Singleton@6bf2d08e
singleton.Singleton@1b701da1

如何解决?

要实现单例,核心有两点:

  • 私有化构造方法,不能让别人 new
  • 提供获取实例的方法

上面的代码就是把原来 private 的构造方法设为 public,然后 new 了下,那知道了这一点,就可以在构造方法那里对应地进行处理

我都饿汉了,实例早就有了,你还来 new,抛个异常给你

1
2
3
4
5
private Singleton() {
if (INSTANCE != null) {
throw new RuntimeException("!");
}
}

再来测试下

1
2
singleton.Singleton@6bf2d08e
Exception in thread "main" java.lang.reflect.InvocationTargetException

再已经有一个实例的情况下,再通过反射来 new,就会得到一个异常

但是,没有其他的问题了吗?再来测试下

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws Exception {
System.out.println(Singleton.getInstance());

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(Singleton.getInstance());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Object obj = ois.readObject();
System.out.println(obj);
}

涉及到了流的操作,单例类需要实现 Serializable 接口

结果如下

1
2
singleton.Singleton@5eb5c224
singleton.Singleton@5ae9a829

又得到了两个不同的实例

如何解决?

1
2
3
private Object readResolve() {
return INSTANCE;
}

再次查看输出结果

1
2
singleton.Singleton@5eb5c224
singleton.Singleton@5eb5c224

解决了,但是为什么 readResolve() 能解决序列化导致两个对象的问题,这个就要稍微看点 JDK 源码(JDK 1.8)

首先从测试代码的 readObject() 入手(删掉不重点关注或者说看不懂的代码,看源码真的要深入每一行吗?你要这么说,那最核心的都是 native 方法)

1
Object obj = ois.readObject();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class: java.io.ObjectInputStream

public final Object readObject()
throws IOException, ClassNotFoundException {
return readObject(Object.class);
}

private final Object readObject(Class<?> type)
throws IOException, ClassNotFoundException
{
...
Object obj = readObject0(type, false);
return obj;
...
}

private Object readObject0(Class<?> type, boolean unshared) throws IOException {
...
case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));
...
}

private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (obj != null &&
handles.lookupException(passHandle) == null &&
// 检查是否有 readResolve 方法
desc.hasReadResolveMethod())
{
// 如果有 readResolve 方法,则返回这个方法返回的对象
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);
}
}

return obj;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class: java.io.ObjectStreamClass

boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}

private ObjectStreamClass(final Class<?> cl) {
...
if (serializable) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
return null;
}
});
}
...
}

上面就是为什么 readResolve() 能防止反序列化出来不同的对象

但是这样,就能实现单例了吗?再来测试下别的

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Exception {
System.out.println(Singleton.getInstance());

Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
System.out.println(unsafe.allocateInstance(Singleton.class));
}
1
2
singleton.Singleton@2a139a55
singleton.Singleton@6d06d69c

这样又能得到两个对象了,因为是比较底层的 Unsafe 类,所以预防不了

那么有没有其他方法,有,就是枚举饿汉

枚举类饿汉式

1
2
3
public enum Singleton1 {
INSTANCE;
}

测试反射破坏单例

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Exception {
System.out.println(Singleton.INSTANCE);

Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Object obj = constructor.newInstance();
System.out.println(obj);
}

得到了一个异常 java.lang.NoSuchMethodException,通过 javap 编译出来的 class 文件,可以看到类的结构

1
2
3
4
5
6
public final class singleton.Singleton extends java.lang.Enum<singleton.Singleton> {
public static final singleton.Singleton INSTANCE;
public static singleton.Singleton[] values();
public static singleton.Singleton valueOf(java.lang.String);
static {};
}

里面确实没有无参构造方法,再看继承的 java.lang.Enum 类

1
2
3
4
5
6
7
8
9
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
...
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
...
}

protected 能被继承,所以枚举类里是有带参的构造方法的,如果调用带参的构造方法,会发生什么

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Exception {
System.out.println(Singleton.INSTANCE);

Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance();
System.out.println(obj);
}
1
2
INSTANCE
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects

可以调用,但是报错了,看回反射的源码,可以看到里面做了一个判断

1
2
3
4
5
6
7
8
9
10
11
12
class: java.lang.reflect.Constructor

@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
...
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
...
}

所以枚举饿汉,是在 JDK 的层面进行了防反射

再试试反序列化

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws Exception {
System.out.println(Singleton.INSTANCE);

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(Singleton.INSTANCE);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Object obj = ois.readObject();
System.out.println(obj);
}
1
2
INSTANCE
INSTANCE

得到的是相同的对象,要了解原因,需要从序列化看起

1
oos.writeObject(Singleton.INSTANCE);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class: java.io.ObjectOutputStream

public final void writeObject(Object obj) throws IOException {
...
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}

private void writeObject0(Object obj, boolean unshared)
throws IOException
{
...
else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
}
...
}

private void writeEnum(Enum<?> en,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
...
// 其实序列化就是存枚举类对象的 name
writeString(en.name(), false);
}

再看反序列化,大部分代码和上面普通类的反序列化相同,这里重点列出不同的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class: java.io.ObjectInputStream

private Object readObject0(Class<?> type, boolean unshared) throws IOException {
...
case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));
...
}

private Enum<?> readEnum(boolean unshared) throws IOException {
...
Enum<?> result = null;
...
// 核心是这行
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
...
return result;
}

public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
// 直接根据 name 拿回枚举类实例,对应上面存的 name,拿到的就是同一个
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}

所以枚举饿汉也是在 JDK 的层面进行了防反序列化

那么 Unsafe 又怎么样

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Exception {
System.out.println(Singleton.INSTANCE);

Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
System.out.println(unsafe.allocateInstance(Singleton.class));
}
1
2
INSTANCE
null

返回了一个 null,因为这个是 native 方法,具体实现的代码看不懂,这里只放出 Java 这边的代码

1
2
3
4
5
6
class: sun.misc.Unsafe

/** Allocate an instance but do not run any constructor.
Initializes the class if it has not yet been. */
public native Object allocateInstance(Class<?> cls)
throws InstantiationException;

所以也可以说,枚举饿汉也能防 Unsafe

普通懒汉式

懒汉就是没有这么饿,但是很懒,因为懒,所以我不提前给你准备好东西,你催我的时候我再来准备,下面的代码都能实现单例,结果就不放了

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static Singleton INSTANCE = null;

public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}

这种方法有个问题,因为 INSTANCE 其实是一个共享变量,共享变量在多线程的环境下就会有资源竞争的问题,所以可能两个线程都会判断 INSTANCE 是 null,所以创建了两个对象

既然这样,那无脑 synchronized 就完了

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static Singleton INSTANCE = null;

public synchronized static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}

可以是可以,但是锁的粒度太大了,并发量很大的情况下性能会有影响(大部分场景下这些都是空话套话),所以要将锁细化

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private static Singleton INSTANCE = null;

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

这样一来,只有 INSTANCE 是 null 的时候才会加锁,性能就会大大提升

但是也会有一个问题,理想情况下是请求一个接着一个来,第一个来了,null,初始化,后面的直接 return,但是在多线程环境下,也会存在两个线程都判断是 null,所以都进去了 if 代码块,虽然只有一个线程能进入 synchronized 代码块,但是一个线程 new 了,就释放锁了,第二个线程还是会 new,这样就会有两个实例

如何解决?在 synchronized 块里再判断一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private static Singleton INSTANCE = null;

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

这样虽然有多个线程判断 INSTANCE 是 null,从而进入 if 代码块,但是只能有一个线程能进入 synchronized 块进行 new 对象,虽然 new 完了别的线程还能进入,但进入后再判断,就不会再 new 了

看似解决了,但是这种方式在 JVM 层面有一个问题

new 对象的时候一般会包括但不限于以下几个步骤:

  • 分配内存空间
  • 变量初始化
  • 返回内存地址(赋值)

但是 JVM 会进行一些优化,江湖人称指令重排序,就是可能会先赋值了再初始化变量,这样以来在多线程的环境下就会得到一个未初始化的对象,那用起来会就有奇怪的问题,为了禁止这个负优化(单例场景下是负优化),需要用 volatile 关键字

volatile 通过增加内存屏障的方式,禁止指令重排序,除此之外,还可以手动增加内存屏障,通过 Unsafe 类的一些 fence 相关的方法(这个不常用,除非是很底层的代码),volatile 还保证可见性,详细的东西可能单独谈比较合适

其他注意的点和饿汉式相同

内部类懒汉式

内部类是类加载的时候就初始化,类加载是线程安全的,其他注意的点也和上面的类似

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private Singleton() {
}

private static final class InstanceHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return InstanceHolder.INSTANCE;
}
}

最后总结:大家都说用枚举类单例好