单例模式

1. 概念

1. 定义

  • 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。

  • 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。

  • 这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。它分为懒汉式和饿汉式。注意:

    • 构造器必须私有化

    • 对外必须获得一个公有的访问方式来获得实例。

2 优缺点

优点:

  • 在内存中只有一个实例,减少了内存开销
  • 可以避免对资源的多重占用,不会出现对同一个文件同时进行写操作
  • 设置全局访问点,严格控制访问

缺点:

  • 没有接口,扩展困难

2. 代码实现

2.1 懒汉式

当程序第一次访问单例模式实例时才进行创建(延迟加载)。

1 单线程实现

缺点:只能在单线程下使用,多线程下会产生多个对象。

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

private LazySingleton() {
}

public static LazySingleton getInstance() {

if (singleton == null) {
singleton = new LazySingleton();
}

return singleton;
}
}

2 加锁多线程

缺点:加锁进行同步,虽然可以保证单例,但效率太低,浪费大量时间。每一个线程进来调用getInstance(),都需要去获取锁

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

private Singleton2() {

}

public static synchronized Singleton2 getInstance() {
if (singleton == null) {
singleton = new Singleton2();
}
return singleton;
}

}

3 双重检查机制

优点:保证单例的同时,也提高了效率。(每一个线程进来调用getInstance(),只有对象不为空,才需要去获取锁)

缺点:会被序列化和反射破坏单例(序列化可处理,反射不可处理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){

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

注意:singleton前面要加volatile关键字来保证程序运行的有序性,否则多线程访问下可能会出现对象未初始化错误!

说明:在内存中,创建一个变量需要三步:1.申请一块内存 ;2.调用构造方法初始化 ;3 分配一个指针指向这块内存

在编译原理中,有一个重要的内容叫做编译器优化,即在不改变原来语义的情况下,调整语句的执行顺序,来让程序运行的更快。因此存在这样一种情况,有两个线程A、B同时访问getInstance方法

  1. A线程判断对象为空,没来得及进行第二次判断,(时间片用完了,B线程进入)

  2. B线程判断对象为空,执行创建变量的3步,先申请一块内存,后分配一个指针指向这块内存,但还没有进行初始化(时间片用完了,A线程进入)

  3. A线程接着执行,发现此时singleton已经不为空了,所以直接返回,但此时返回的singleton对象虽然B线程已经new了,但还没有初始化这个实例并没有构造完成,此时如果A线程使用这个实例,程序就会出现对象未初始化错误了。

2.2 饿汉式

顾名思义,饿汉法就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。

优点:写法简单,在类加载的时候就完成实例化,避免线程同步。

1 简单写法

缺点:没有达到懒加载的效果,若果自始至终都没有用过这个对象,就会造成内存浪费。

1
2
3
4
5
6
7
8
9
public class HungrySingleton{

private final static HungrySingleton hungrySingleton= new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}

2 静态内部类

这里采用了静态内部类实例singleton对象,静态内部类相当于一个静态属性,只有在第一次加载类时才会初始化,在类初始化时,别的线程是无法进入的,因此保证了线程安全。

缺点:会被序列化和反射破坏单例(序列化可处理,反射不可处理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}

// 避免反射攻击
private StaticInnerClassSingleton(){
if(InnerClass.staticInnerClassSingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
// 避免序列化破坏单例
private Object readResolve(){
return InnerClass.staticInnerClassSingleton;
}
}

3 枚举单例(推荐)

对于这种方式的单例,不受反射和序列化的攻击,是最推荐的一种写法。

1
单元素的枚举类型已经成为实现Singleton的最佳方法               	-- 出自 《effective java》
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum EnumInstance {
INSTANCE;
private Object data;

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}

测试序列化和反序列化。可以看出枚举不受序列化影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestEnum {
public static void main(String[] args) throws Exception {
EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
EnumInstance newInstance = (EnumInstance) ois.readObject();

System.out.println(instance.getData());
System.out.println(newInstance.getData());
System.out.println(instance.getData() == newInstance.getData());
}
}

image-20231003161934648

测试反射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestEnumReflect {
public static void main(String[] args) throws Exception {
EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());

// enum没有无参构造器
Constructor<EnumInstance> constructor = EnumInstance.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumInstance newInstance = constructor.newInstance("test", 111);

System.out.println(instance.getData());
System.out.println(newInstance.getData());
System.out.println(instance.getData() == newInstance.getData());
}
}

image-20231003163510478

可以看到,报错 Cannot reflectively create enum objects,无法通过反射创建枚举对象

3. 序列化和反射破坏单例

3.1 序列化破坏单例

1
2
3
4
5
6
7
8
public class HungrySingleton implements Serializable{

private final static HungrySingleton hungrySingleton= new HungrySingleton();

public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);

File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newInstance = (HungrySingleton) ois.readObject();

System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}

image-20231003124316573

解决方式:

HungrySingleton 增加readResolve()方法

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

3.2 反射破坏单例

只可避免反射破坏饿汉式单例,无法避免反射破坏懒汉式单例。

1. 饿汉式单例

对于饿汉式,即在类初始化时创建对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestReflect {
public static void main(String[] args) throws Exception {
HungrySingleton instance = HungrySingleton.getInstance();

Constructor<HungrySingleton> constructor = HungrySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton newInstance = constructor.newInstance();

System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}

image-20231003145832779

由上可看出,通过反射创建了两个不同的实例。可做如下调整:

当使用无参构造器构建对象时,由于对象已经在类加载时创建,则不允许再创建新对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HungrySingleton implements Serializable,Cloneable{

private final static HungrySingleton hungrySingleton;

static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
if(hungrySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}

2. 懒汉式单例

即使在私有构造器中,增加了禁止反射调用的逻辑,也无法确保单例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
if(lazySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
  • 当先通过反射创建对象,再通过方法调用创建对象的场景,就会产生两个不同的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestReflect2 {
public static void main(String[] args) throws Exception {
Constructor<LazySingleton> constructor = LazySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton newInstance = constructor.newInstance();

LazySingleton instance = LazySingleton.getInstance();

System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}

image-20231003172322321

当先通过方法调用创建对象的场景,再通过反射创建对象,则会根据逻辑阻止第二个对象的产生。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestReflect2 {
public static void main(String[] args) throws Exception {
LazySingleton instance = LazySingleton.getInstance();

Constructor<LazySingleton> constructor = LazySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton newInstance = constructor.newInstance();

System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}

image-20231003172354599

因为二者虽然都会调用无参构造器,但是只有getInstance()方法才会将lazySingleton赋值。


单例模式
http://example.com/单例模式/
作者
Panyurou
发布于
2023年9月26日
许可协议