Java单例模式

方式一:饿汉式

介绍

所谓饿汉式单例模式,就是在第一次获取单例对象之前(而不是获取时),就创建好了这个对象,比如下图的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singtelon1 {
private static final Singtelon1 INSTANCE = new Singtelon1();

private Singtelon1(){
System.out.println("Singleton1 is constructed");
}

public static Singtelon1 getInstance(){
return INSTANCE;
}

public static void doSomething(){
System.out.println("Singleton1 is doing something");
}
}

将构造方法私有就是为了防止其他类调用,从而在一定程度上保证了单例。现在我们可以做一下测试,看下如果我尝试通过getInstance获取两次对象,对象是否是同一个:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) throws Exception{
Singtelon1.doSomething();
System.out.println("-------------------------------------------");
System.out.println(Singtelon1.getInstance());
System.out.println(Singtelon1.getInstance());
}
}

运行出的结果:

1
2
3
4
5
6
7
Singleton1 is constructed
Singleton1 is doing something
-------------------------------------------
Singtelon1@3b07d329
Singtelon1@3b07d329

Process finished with exit code 0

可以看出,两次调用getInstance获取到的对象的哈希值是一致的,我们可以认为他们是同一个对象。这里的结果还说明了,对象是在类加载(第一次被使用)后就创建出的,因为Singleton1 is constructedSingleton1 is doing something之前,而Singleton1 is doing something与获取对象和使用对象都没关系。

破坏饿汉式单例模式的方法

方法一:反射

众所周知,我们可以通过反射获取构造方法,并给予可执行的权限,这样我们就可以在别的类中调用构造函数了!单例模式就这么被破坏掉了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Main {
public static void main(String[] args) throws Exception{
Singtelon1.doSomething();
System.out.println("-------------------------------------------");
System.out.println(Singtelon1.getInstance());
System.out.println(Singtelon1.getInstance());

reflection(Singtelon1.class);
}

private static void reflection(Class<?> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println("反射创建对象:" + constructor.newInstance());
}
}

结果为:

1
2
3
4
5
6
7
8
Singleton1 is constructed
Singleton1 is doing something
-------------------------------------------
Singtelon1@3b07d329
Singtelon1@3b07d329
反射创建对象:Singtelon1@3d075dc0

Process finished with exit code 0

从结果可以看出,我们的确构造了两个不同的Singleton1对象,这就破坏了单例模式。但是反射破坏的单例模式并非是无解的!我们可以这么修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singtelon1 {
private static final Singtelon1 INSTANCE = new Singtelon1();

private Singtelon1(){
if(INSTANCE == null)
System.out.println("Singleton1 is constructed");
else
throw new RuntimeException("Singleton1 was constructed!!!");
}

public static Singtelon1 getInstance(){
return INSTANCE;
}

public static void doSomething(){
System.out.println("Singleton1 is doing something");
}
}

再来测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Singleton1 is constructed
Singleton1 is doing something
-------------------------------------------
Singtelon1@3b07d329
Singtelon1@3b07d329
Exception in thread "main" java.lang.reflect.InvocationTargetException
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
at Main.reflection(Main.java:17)
at Main.main(Main.java:11)
Caused by: java.lang.RuntimeException: Singleton1 was constructed!!!
at Singtelon1.<init>(Singtelon1.java:8)
... 7 more

Process finished with exit code 1

可以看出,当我们尝试使用反射来构造Singtelon1的时候,代码是报错了的!这就解决了问题。

方法二:反序列化

如果Singtelon1实现了Serializable接口,那么这个Singtelon1就可以通过反序列化的方式得到一个新的对象,甚至不需要构造方法!反序列化有很多应用,在这里不进行说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.Serializable;

public class Singtelon1 implements Serializable{
private static final Singtelon1 INSTANCE = new Singtelon1();

private Singtelon1(){
System.out.println("Singleton1 is constructed");
}

public static Singtelon1 getInstance(){
return INSTANCE;
}

public static void doSomething(){
System.out.println("Singleton1 is doing something");
}
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) throws Exception{
Singtelon1.doSomething();
System.out.println("-------------------------------------------");
System.out.println(Singtelon1.getInstance());
System.out.println(Singtelon1.getInstance());

serialization(Singtelon1.getInstance());
}

private static void serialization(Object instance) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
System.out.println("反序列化创建对象:" + ois.readObject());
}
}

结果:

1
2
3
4
5
6
7
8
Singleton1 is constructed
Singleton1 is doing something
-------------------------------------------
Singtelon1@3b07d329
Singtelon1@3b07d329
反序列化创建对象:Singtelon1@2957fcb0

Process finished with exit code 0

可以看出,反序列化不仅可以创建一个新的对象,而且还不需要调用构造方法!(因为我们得到了两个Singleton1对象,但是

Singleton1 is constructed却只输出了一次)。幸运的是,这种破坏单例模式的方法也是可以解决的

只需要我们在Singleton1类中增加一个readResolve方法:

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

然后问题就被解决了:

1
2
3
4
5
6
7
8
Singleton1 is constructed
Singleton1 is doing something
-------------------------------------------
Singtelon1@3b07d329
Singtelon1@3b07d329
反序列化创建对象:Singtelon1@3b07d329

Process finished with exit code 0

原因是这样会使ois.readObject()执行的返回结果为INSTANCE

方法三:unsafe

unsafe是jdk中内置的一个类,我们并不能直接访问,但我们可以利用反射来访问。而unsafe也可以通过不调用构造方法的方式得到对应类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class Main {
public static void main(String[] args) throws Exception{
Singtelon1.doSomething();
System.out.println("-------------------------------------------");
System.out.println(Singtelon1.getInstance());
System.out.println(Singtelon1.getInstance());

unsafe(Singtelon1.class);
}

private static void unsafe(Class<?> clazz) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Object instance = unsafe.allocateInstance(clazz);
System.out.println("unsafe创建对象: " + instance);
}
}

结果为:

1
2
3
4
5
6
7
8
Singleton1 is constructed
Singleton1 is doing something
-------------------------------------------
Singtelon1@3b07d329
Singtelon1@3b07d329
unsafe创建对象: Singtelon1@3b9a45b3

Process finished with exit code 0

可惜的是,这种方法目前还没找到比较好的解决办法。

方式二:枚举(饿汉式)

介绍

枚举类在编译后的字节码中,对每个选项都进行了饿汉式的创建,并在获取选项的时候是单例的,即不会再次创建。那么,假如我们的枚举类中只有一个选项,那么不就实现了单例模式吗?让我们看下下边的例子:

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
// 单例集合
public enum Singtelon2 {
INSTANCE;

Singtelon2(){ // 枚举类的构造函数,默认是private的,所以可以去掉
System.out.println("Singleton2 is constructed");
}

@Override
public String toString(){ // 打印枚举类时,默认会打印类名,而没有hashCode,因此要重写toString
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

public static Singtelon2 getInstance(){
return INSTANCE;
}

public static void doSomething(){
System.out.println("Singleton2 is doing something");
}
}

//测试代码
public class Main {
public static void main(String[] args) {
Singtelon2.doSomething();
System.out.println("-------------------------------------------");
System.out.println(Singtelon2.getInstance());
System.out.println(Singtelon2.getInstance());
}
}

结果为:

1
2
3
4
5
6
7
Singleton2 is constructed
Singleton2 is doing something
-------------------------------------------
Singtelon2@3b07d329
Singtelon2@3b07d329

Process finished with exit code 0

显然,两次获取的对象是同一个,并且对象是在类加载(第一次被使用)后就创建出的,也就是用饿汉式创建出的。

破坏枚举单例模式的方法

因为都是饿汉式的,所以破坏方式也和方式一中的一样:反射、反序列化、unsafe,是这样的吗?

方法一:反射(不可行)

我们先仿照方式一中的代码试一试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Main {
public static void main(String[] args) throws Exception {
Singtelon2.doSomething();
System.out.println("-------------------------------------------");
System.out.println(Singtelon2.getInstance());
System.out.println(Singtelon2.getInstance());

reflection(Singtelon2.class);
}

private static void reflection(Class<?> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println("反射创建对象:" + constructor.newInstance());
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
Singleton2 is constructed
Singleton2 is doing something
-------------------------------------------
Singtelon2@3b07d329
Singtelon2@3b07d329
Exception in thread "main" java.lang.NoSuchMethodException: Singtelon2.<init>()
at java.base/java.lang.Class.getConstructor0(Class.java:3585)
at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2754)
at Main.reflection(Main.java:15)
at Main.main(Main.java:11)

Process finished with exit code 1

可以看到,代码报错了,这是因为我们在获取构造方法时除了问题。枚举类的构造方法,实际上是有两个参数的:枚举选项的名字(String类型)和选项的编号(int类型),所以我们修改代码:

1
2
3
4
5
private static void reflection(Class<?> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
System.out.println("反射创建对象:" + constructor.newInstance("OTHER",1));
}

然而结果为:

1
2
3
4
5
6
7
8
9
10
11
12
Singleton2 is constructed
Singleton2 is doing something
-------------------------------------------
Singtelon2@3b07d329
Singtelon2@3b07d329
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
at Main.reflection(Main.java:17)
at Main.main(Main.java:11)

Process finished with exit code 1

可以看出,用反射来构造枚举类中的对象时,代码会报错,所以反射破坏不了枚举的单例模式。

方法二:反序列化(不可行)

直接上测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.*;

public class Main {
public static void main(String[] args) throws Exception{
Singtelon2.doSomething();
System.out.println("-------------------------------------------");
System.out.println(Singtelon2.getInstance());
System.out.println(Singtelon2.getInstance());

serialization(Singtelon2.getInstance());
}

private static void serialization(Object instance) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
System.out.println("反序列化创建对象:" + ois.readObject());
}
}

结果:

1
2
3
4
5
6
7
8
Singleton2 is constructed
Singleton2 is doing something
-------------------------------------------
Singtelon2@3b07d329
Singtelon2@3b07d329
反序列化创建对象:Singtelon2@3b07d329

Process finished with exit code 0

可以看出,这里的反序列化没有破坏单例模式!!!

方法三:unsafe

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class Main {
public static void main(String[] args) throws Exception{
Singtelon2.doSomething();
System.out.println("-------------------------------------------");
System.out.println(Singtelon2.getInstance());
System.out.println(Singtelon2.getInstance());

unsafe(Singtelon2.class);
}

private static void unsafe(Class<?> clazz) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Object instance = unsafe.allocateInstance(clazz);
System.out.println("unsafe创建对象: " + instance);
}
}

结果为:

1
2
3
4
5
6
7
8
Singleton2 is constructed
Singleton2 is doing something
-------------------------------------------
Singtelon2@3b07d329
Singtelon2@3b07d329
unsafe创建对象: Singtelon2@5f184fc6

Process finished with exit code 0

同样的,这种方法不仅可以破坏单例,还没有找到解决办法。

方式三:懒汉式

介绍

饿汉式单例模式在类的在第一次获取单例对象之前(而不是获取时),就创建好了这个对象。而如果我们一直没有使用获取这个对象的方法,那就会造成内存的浪费,为了减少内存的浪费,我们可以采用懒汉式的单例模式,即当第一次获取单例对象时,才去创建对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singtelon3 {
private static Singtelon3 INSTANCE;

private Singtelon3(){
System.out.println("Singleton is constructed");
}

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

public static void doSomething(){
System.out.println("Singleton3 is doing something");
}
}

做一个测试:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
Singtelon3.doSomething();
System.out.println("-------------------------------------------");
System.out.println(Singtelon3.getInstance());
System.out.println(Singtelon3.getInstance());
}
}

结果:

1
2
3
4
5
6
7
Singleton3 is doing something
-------------------------------------------
Singleton3 is constructed
Singtelon3@3b07d329
Singtelon3@3b07d329

Process finished with exit code 0

可以看出,这种方式的确可以保证单例(两次调用getInstance获取对象的哈希值一致),并且,类加载后并没有立即创建对象,而是在第一次获取对象的时候才对对象进行创建。这就是懒汉式的单例模式。

懒汉式在多线程中的问题

但是,在多线程的环境中,这种模式就有很大的问题了:

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Singtelon3.getInstance());
});
Thread t2 = new Thread(() -> {
System.out.println(Singtelon3.getInstance());
});
t1.start();
t2.start();
}
}

执行上述代码,得到的结果是:

1
2
3
4
5
6
Singleton3 is constructed
Singtelon3@63236bfa
Singleton3 is constructed
Singtelon3@24224129

Process finished with exit code 0

可以看到,单例模式被破坏了!

原因可能有很多,总体来说就是下列代码中,两个线程都进入了if为true的代码中,然后各自创建了对象。而导致两个线程都进入了if为true的代码中的原因可能有很多,比如一个刚进去,就切换到另一个,这时还没创建好对象,判断结果还是true,另一个又进去了;或者是一个线程刚创建好对象,还没赋值给INSTANCE,就切换到另一个线程了,另一个线程判断结果还是true,然后又能执行new Singtelon3()操作了,等等。

1
2
3
4
5
public static Singtelon3 getInstance(){
if(INSTANCE == null)
INSTANCE = new Singtelon3();
return INSTANCE;
}

解决方法:双重检查锁

问题出在两个线程都进入了if为true的代码中,然后各自创建了对象,因此我们只要使得,两个线程里只能有一个线程创建对象就好了,因此就得到了如下代码:

1
2
3
4
5
6
7
8
9
public static Singtelon3 getInstance(){
if(INSTANCE == null){
synchronized (Singtelon3.class){
if(INSTANCE == null)
INSTANCE = new Singtelon3();
}
}
return INSTANCE;
}

当INSTANCE不为空:直接返回;当INSTANCE为空:先去尝试获得锁,在成功获得锁之前,可能已经有其他线程进入同步代码块创建对象了,因此进入同步代码块后,需要再次判断是否为空。

为什么要加volatile

但这里需要注意的是,这里的INSTANCE一定得是加了volatile关键字的原因如下:

  1. 为了保证共享变量INSTANCE的可见性,在为INSTANCE赋值后能让其他线程立即看到。这个原因并不是主要原因,因为INSTANCE == null的判断会去获取INSTANCE的值,所以其实不加volatile并不会出现可见性的问题。(做测试发现确实没有出现问题)

    1
    2
    3
    4
    5
    Singleton3 is constructed
    Singtelon3@7a092293
    Singtelon3@7a092293

    Process finished with exit code 0
  2. 为了保证编译代码的有序性。这是最核心的原因。如果不加volatile,则INSTANCE = new Singtelon3();的编译结果未必是我们想的那样。使用javap -c -v -p Singtelon3.class查看一下Singtelon3.class的反编译:

    1
    2
    3
    4
    17: new           #22                 // class Singtelon3    			分配内存空间
    20: dup
    21: invokespecial #27 // Method "<init>":()V 对象内初始化赋值
    24: putstatic #21 // Field INSTANCE:LSingtelon3; 把对象赋值给INSTANCE

    可以看到以上三步,分别是分配空间,给对象初始化赋值,和把对象地址赋值给INSTANCE。分配空间肯定是第一步做的,但是后两步都是赋值,因此编译的代码可能会改变两者的顺序。这就有问题了:如果先执行了putstatic ,然后另一个线程就去进行INSTANCE == null的检测,发现不为null(因为已经赋值给INSTANCE了),但此时还没有对对象进行初始化,那么拿到的INSTANCE其实是有问题的,这就很危险了。所以,我们需要保证 invokespecial一定在putstatic之前执行。而因为volatile有内存屏障,所以可以保证 invokespecial一定在putstatic之前执行。这才是为什么我们要给INSTANCE加volatile。

方式四:内部类(懒汉式)

我们知道,private static final Singtelon1 INSTANCE = new Singtelon1();这一步是线程安全的,这是因为这一步是在类的初始化时给静态变量赋值,是在静态代码块中执行的,而静态代码块会维护线程安全,因此只要我们也能通过静态代码块懒汉式地构建对象,那么不就不需要双重检查锁了吗?要实现这件事,就需要我们执行private static final Singtelon1 INSTANCE = new Singtelon1(),但我们又不希望在这之前执行类的方法时就对类初始化,所以我们可以通过内部类的方式来实现这一点。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singtelon4 {
private Singtelon4(){
System.out.println("Singleton4 is constructed");
}

private static class Holder{
static Singtelon4 INSTANCE = new Singtelon4();
}

public static Singtelon4 getInstance(){
return Holder.INSTANCE;
}

public static void doSomething(){
System.out.println("Singleton4 is doing something");
}
}

只要我们不去使用getInstance方法,Holder就不会执行类初始化,也就不会创建Singtelon4对象,而其创建Singtelon4对象并复制的过程是在静态代码块中的,是线程安全的,因此我们轻松地实现了懒汉式单例模式。

测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
Singtelon4.doSomething();
System.out.println("----------------------------------");
Thread t1 = new Thread(() -> {
System.out.println(Singtelon4.getInstance());
});
Thread t2 = new Thread(() -> {
System.out.println(Singtelon4.getInstance());
});
t1.start();
t2.start();
}
}

结果如下:

1
2
3
4
5
6
7
Singleton4 is doing something
----------------------------------
Singleton4 is constructed
Singtelon4@5635b27f
Singtelon4@5635b27f

Process finished with exit code 0

可以看出,当我执行doSomething()时,并不会引发单例对象的构建,而当我多线程地获取单例对象时,单例对象也只被创建了一次,也就是说,我们用这种方式,实现了懒汉式单例模式。

单例模式在JDK中的体现

项目中使用单例模式是危险的,因为可能会用错。但是有道这样的面试题:

在哪里使用了单例模式 ?

既然我们没能在项目使用单例模式,我们就不能举项目中的例子了。我们可以把目光投向JDK和框架上,在这里,我会介绍单例模式在JDK中的一些体现。

例一:Runtime类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package java.lang;

public class Runtime {
private static final Runtime currentRuntime = new Runtime();

private static Version version;

/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class {@code Runtime} are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the {@code Runtime} object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}

/** Don't let anyone else instantiate this class */
private Runtime() {}

不难看出,JDK里的Runtime类是用了单例模式的,而且是我们提到的饿汉式单例模式。

当我们要退出JVM时,会调用exit方法,这个方法是在System类中的,这个方法的代码如下:

1
2
3
public static void exit(int status) {
Runtime.getRuntime().exit(status);
}

可以看出,这里是通过Runtime.getRuntime()获取到的Runtime单例对象。

除了exit之外,System类中还有很多别的方法也是通过Runtime.getRuntime()获取单例对象来调用Runtime类中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package java.lang;

public final class System {
......

public static void exit(int status) {
Runtime.getRuntime().exit(status);
}

public static void gc() {
Runtime.getRuntime().gc();
}

public static void runFinalization() {
Runtime.getRuntime().runFinalization();
}

@CallerSensitive
public static void load(String filename) {
Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}

例二:Console类

刚刚提到了System类,这个类有个Console对象,也实现了单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static volatile Console cons;

/**
* Returns the unique {@link java.io.Console Console} object associated
* with the current Java virtual machine, if any.
*
* @return The system console, if any, otherwise {@code null}.
*
* @since 1.6
*/
public static Console console() {
Console c;
if ((c = cons) == null) {
synchronized (System.class) {
if ((c = cons) == null) {
cons = c = SharedSecrets.getJavaIOAccess().console();
}
}
}
return c;
}

这是我能找到的JDK中的唯一一个懒汉式单例模式了。

需要注意的是,这里的单例对象是Console的对象,但是是写在System中的,这也是为什么对cons的赋值并不是直接new,而是一定要用SharedSecrets.getJavaIOAccess().console()

在Console类中,Console对象也是单例的:

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
private static final Charset CHARSET;
static {
String csname = encoding();
Charset cs = null;
if (csname == null) {
csname = GetPropertyAction.privilegedGetProperty("sun.stdout.encoding");
}
if (csname != null) {
try {
cs = Charset.forName(csname);
} catch (Exception ignored) { }
}
CHARSET = cs == null ? Charset.defaultCharset() : cs;

// Set up JavaIOAccess in SharedSecrets
SharedSecrets.setJavaIOAccess(new JavaIOAccess() {
public Console console() {
if (istty()) {
if (cons == null)
cons = new Console();
return cons;
}
return null;
}

public Charset charset() {
return CHARSET;
}
});
}
private static Console cons;
private static native boolean istty();
private Console() {

例三:Collections类

Conllections类是一个集合工具类,我们可以通过这个类来获取空的集合,这时的空集合就用到了单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static class EmptyNavigableSet<E> extends UnmodifiableNavigableSet<E>
implements Serializable {
@java.io.Serial
private static final long serialVersionUID = -6291252904449939134L;

public EmptyNavigableSet() {
super(new TreeSet<>());
}

@java.io.Serial
private Object readResolve() { return EMPTY_NAVIGABLE_SET; }
}

@SuppressWarnings("rawtypes")
private static final NavigableSet<?> EMPTY_NAVIGABLE_SET =
new EmptyNavigableSet<>();

而且可以看到,为了保护类的单例,还实现了Serializable,从而避免了反序列化破坏单例。这种获取空集合的方式,在Conllections类中还有很多,在此不一一列举了。

除了直接把对象赋值给Conllections类的静态成员变量,Collections类内部还有很多内部类,这些内部类也运用了单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static <T> Iterator<T> emptyIterator() {
return (Iterator<T>) EmptyIterator.EMPTY_ITERATOR;
}

private static class EmptyIterator<E> implements Iterator<E> {
static final EmptyIterator<Object> EMPTY_ITERATOR
= new EmptyIterator<>();

public boolean hasNext() { return false; }
public E next() { throw new NoSuchElementException(); }
public void remove() { throw new IllegalStateException(); }
@Override
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
}
}

顺藤摸瓜,我们还可以找到枚举饿汉式的例子:

1
2
3
public static <T> Comparator<T> reverseOrder() {
return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
}

Conllections类中的reverseOrder()方法,返回值是ReverseComparator类的REVERSE_ORDER,这是一个单例对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static class ReverseComparator
implements Comparator<Comparable<Object>>, Serializable {

@java.io.Serial
private static final long serialVersionUID = 7207038068494060240L;

static final ReverseComparator REVERSE_ORDER
= new ReverseComparator();

public int compare(Comparable<Object> c1, Comparable<Object> c2) {
return c2.compareTo(c1);
}

@java.io.Serial
private Object readResolve() { return Collections.reverseOrder(); }

@Override
public Comparator<Comparable<Object>> reversed() {
return Comparator.naturalOrder();
}
}

而Comparator的naturalOrder方法的返回值,也是单例的:

1
2
3
public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Comparators {
private Comparators() {
throw new AssertionError("no instances");
}

/**
* Compares {@link Comparable} objects in natural order.
*
* @see Comparable
*/
enum NaturalOrderComparator implements Comparator<Comparable<Object>> {
INSTANCE;

@Override
public int compare(Comparable<Object> c1, Comparable<Object> c2) {
return c1.compareTo(c2);
}

@Override
public Comparator<Comparable<Object>> reversed() {
return Comparator.reverseOrder();
}
}

可以看出,这里的INSTANCE是枚举类NaturalOrderComparator的唯一一个选项,这就是通过枚举实现饿汉式单例模式的体现。


Java单例模式
https://jlqusername.github.io/2025/02/23/Java单例模式/
作者
B907
发布于
2025年2月23日
许可协议