0%

Java实现单例模式

单例模式(Singleton Pattern)即一个JVM内存中只存在一个类的实例对象。这种类型的设计模式属于创建型模式。

这种设计模式涉及到单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建,这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该对象。

  • 单例类只能有一个实例
  • 单例类必须自己创建自己的唯一实例
  • 单例类必须给所有其他对象提供这一实例
  • 判断系统是否已经有这个实例,如果有则返回,没有则创建
  • 构造函数是私有的
  • 没有接口、不能继承
  • getInstance()方法需要使用同步锁synchronized防止多线程同时进入造成instance被多次实例化

1

懒汉式,线程不安全

不支持多线程,因为没有加锁

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

private Singleton() {
} //构造器私有,其他类就无法通过new Singleton()来创建实例对象

public static Singleton getInstance() {
//获取实例
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

懒汉式,线程安全

能够在多线程中很好的工作,但是效率很低。第一次调用才初始化,即懒加载(lazy loading),避免内存浪费。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private static Singleton instance;//只是声明,默认null,不会在堆内构建实例

private Singleton() {
}

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

饿汉式

这种方法比较常用,但是容易产生垃圾对象。没有加锁,会提升效率,但类加载时就初始化,浪费内存。

1
2
3
4
5
6
7
8
9
public class Singleton {
private static Singleton instance = new Singleton();

private Singleton() {}

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

饿汉式变种

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}

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

双重检验锁

安全且在多线程情况下保持高性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {
private volatile static Singleton singleton;
//volatile 避免重排序
private Singleton() {}

public static Singleton getSingleton() {
if (singleton == null) { //判断是否为空
synchronized (Singleton.class) { //加锁
if (singleton == null) { //只能有一个进入
//如果A进入之后实例化,B通过第一个判断再次进入条件不成立
singleton = new Singleton();
}
}
}
return singleton;
}
}

静态内部类

能达到双检锁一样的功效,对静态使用延迟初始化,只适应于静态域的情况,双检锁可以在实例域需要初始化时延迟使用

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}

public static Singleton getInstance() {
return SingletonHolder.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.io.*;
import java.lang.*;

public enum Singleton {
INSTANCE;

public void whateverMethod() {
System.out.println("hhh");
}
}
public class Test {

public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton.INSTANCE.whateverMethod();
// 简单引用
Singleton instance0 = Singleton.INSTANCE;
Singleton instance1 = Singleton.INSTANCE;
System.out.println("instance0===" + instance0.hashCode());
System.out.println("instance1===" + instance1.hashCode());
// 反射测试
Class clazz = Singleton.class;
Singleton instance2 = (Singleton) Enum.valueOf(clazz, "INSTANCE");
Singleton instance3 = (Singleton) Enum.valueOf(clazz, "INSTANCE");
System.out.println("instance2===" + instance2.hashCode());
System.out.println("instance3===" + instance3.hashCode());
// 序列化测试
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("test")));
oos.writeObject(instance0);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("test")));
Singleton instance4 = (Singleton) ois.readObject();
ois.close();
ObjectInputStream ois1 = new ObjectInputStream(new FileInputStream(new File("test")));
Singleton instance5 = (Singleton) ois1.readObject();
ois1.close();
System.out.println("instance4===" + instance4.hashCode());
System.out.println("instance5===" + instance5.hashCode());
}

}

//结果
hhh
instance0===1927950199
instance1===1927950199
instance2===1927950199
instance3===1927950199
instance4===1927950199
instance5===1927950199

1.为什么构造函数要使用private

构造器私有,其他类就无法通过new Singleton()来创建对象实例

2.为什么双重校验锁使用volatile和两次判空检验

第一个 if 判断是为了减少性能开销

第二个 if 判断是为了避免生成多个对象实例。

volatile为了禁止 JVM 的指令重排,指令重排会导致对象未初始化的情况,造成报错

3.单例模式中唯一实例为什么要用静态

因为getInstance()是静态方法,而静态方法不能访问非静态成员变量,所以instance只能使用静态

3.1为什么getInstance()是静态方法

因为构造器是私有的,程序调用类中方法只有两种方式,

① 创建类的一个对象,用该对象去调用类中方法;

② 使用类名直接调用类中方法,格式“类名.方法名()”;

Singleton instance = Singleton.getInstance();

构造函数私有化后第一种情况就不能用,只能使用第二种方法。

3.2为什么要私有化构造器

目的是禁止其他程序创建该类的对象

如果构造函数不是私有的,每个人都可以通过 new Singleton() 创建类的实例,因此不再是单例。根据定义,对于一个单例,只能存在一个实例。

4.懒汉式与饿汉式区别

懒汉式:先天性线程不安全,当真正需要该实例的时候才去加载,需要我们自己人为上锁控制线程安全问题。

饿汉式:先天性线程安全,当我们项目在启动的时候创建该实例,会导致项目启动比较慢

5.静态内部类与双重校验锁的区别?

静态内部类使用静态关键字去保证我们实例是单例的。

而我们的双重校验锁采用 lock 锁保证安全的。

6.为什么静态内部类写法中,静态类里面获取单例对象要用final修饰

用 final 更多的意义在于提供语法约束。毕竟你是单例,就只有这一个实例,不可能再指向另一个实例。instance有了 final 的约束,后面再有人不小心编写了修改其指向的代码就会报语法错误。

7.单例饿汉式为什么没有线程安全性问题

getInstance()获取的实例方法中,没有对资源进行非原子性操作,instance在类加载过程中就被实例化了。即只要不对instance修改,返回的永远是同一个对象,因为instance是private的,所以不需要加锁

破坏单例模式

2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
//对静态内部类进行操作,使用两次构造函数
public static void main(String[] args) {
try {
// 很无聊的情况下,进行破坏
Class<?> clazz = Singleton.class;
// 通过反射拿到私有的构造方法
Constructor c = clazz.getDeclaredConstructor();
// 因为要访问私有的构造方法,这里要设为true,相当于让你有权限去操作
c.setAccessible(true);
// 暴力初始化
Object o1 = c.newInstance();
// 调用了两次构造方法,相当于 new 了两次
Object o2 = c.newInstance();
// 这里输出结果为false
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
防止反射破坏单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Singleton {

// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

// 私有的构造方法
private Singleton() {
// 防止反射创建多个对象
if(SingletonHolder.INSTANCE != null){
throw new RuntimeException("不允许创建多个实例");
}
}

// 公有的获取实例方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}

}

参考资料:

Java 单例模式从入门到入坟