> 文章列表 > Java单例模式

Java单例模式

Java单例模式

单例模式

开发环境:JDK 11

创建型模式

定义:保证一个类只有一个实例,并且提供全局访问点。

常见的五种单例模式实现方式主要:

简单:

  • 饿汉式(线程安全,调用效率高;但是不能延时加载
  • 懒汉式(线程安全,调用效率不高,但是可以延时加载)

常用:

  • 双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题,不建议使用)
  • 静态内部类式(线程安全,调用效率高,但是,可以延时加载)

其他

  • 枚举类(线程安全,调用效率高,防反射和反序列化,不能延时加载)

如何选用?

  • 单例对象 占用资源少,不需要延时加载时: 枚举类 好于 饿汉式
  • 单例对象 占用资源大,需要延时加载时: 静态内部类式 好于 懒汉式

懒汉模式

public class Sluggard {private static Sluggard sInstance;private Sluggard() {}public static Sluggard getInstance() {if (sInstance == null) {sInstance = new Sluggard();}return sInstance;}
}

验证

// 单线程懒汉模式
private static void SingleThreadSlackerMode() {Sluggard instance1 = Sluggard.getInstance();Sluggard instance2 = Sluggard.getInstance();log(instance1.toString());log(instance2.toString());
}

输出
Java单例模式

// 多线程懒汉模式
private static void multiThreadSlackerMode() {new Thread(() -> {Sluggard instance1 = Sluggard.getInstance();log(instance1.toString());}).start();new Thread(() -> {Sluggard instance2 = Sluggard.getInstance();log(instance2.toString());}).start();
}

Java单例模式

优化多线程懒汉模式

最简单的优化,方法加synchronized

public synchronized static Sluggard getInstance() {if (sInstance == null) {sInstance = new Sluggard();}return sInstance;
}

加上时间对比

private static void singleThreadSlackerMode() {log("start Time: " + System.currentTimeMillis());Sluggard instance1 = Sluggard.getInstance();log("end   Time: " + System.currentTimeMillis());Sluggard instance2 = Sluggard.getInstance();log(instance1.toString());log(instance2.toString());
}

单线程下,没有synchronized的时间组

start Time: 1669960761198
end Time: 1669960761220 差值 22

start Time: 1669960836708
end Time: 1669960836730 差值 22

start Time: 1669960852883
end Time: 1669960852904 差值 21

start Time: 1669960866630
end Time: 1669960866652 差值 22

单线程下,有synchronized的时间组

start Time: 1669960890940
end Time: 1669960890962 差值 22

start Time: 1669960941323
end Time: 1669960941344 差值 21

start Time: 1669960961080
end Time: 1669960961101 差值 21

start Time: 1669960998264
end Time: 1669960998284 差值 20

start Time: 1669961026460
end Time: 1669961026482 差值 22

从上面的数据,不考虑CPU等环境得到的结论是:

创建一个对象时间大概是22,而且synchronized影响不大。

同时我做了JDK1.8带synchronized的测试

结果如下:

start Time: 1669961521357
end Time: 1669961521357

start Time: 1669961574984
end Time: 1669961574985

start Time: 1669961582302
end Time: 1669961582303

start Time: 1669961627128
end Time: 1669961627128

不带synchronized的时间基本都是一样的

start Time: 1669961655940
end Time: 1669961655940

start Time: 1669961685226
end Time: 1669961685226

start Time: 1669961691910
end Time: 1669961691910

以上测试,只作为讨论,可能因为测试写法、精度、环境、数量的影响,不能代表synchronized没有耗时。

这段代码在getInstance()方法前面加了synchronized关键字,使用了类级别的锁,即在整个类上进行同步控制。这种方式可以保证在多线程环境下只有一个线程会执行getInstance()方法,但是在第一次调用getInstance()方法时会进行同步控制,即使已经创建了对象,也会对整个方法进行同步控制,影响了性能。

延迟synchronized

public class Sluggard {private static Sluggard sInstance;private Sluggard() {}public static Sluggard getInstance() {if (sInstance == null) {synchronized (Sluggard.class) {sInstance = new Sluggard();}}return sInstance;}
}

这个时候sInstance不等于null的时候就会直接返回了,并不需要通过synchronized。

当然这有一个问题,多线程下其实是两个对象

Java单例模式

问题在哪里?
Java单例模式

这个时候可以两个线程同时进来

双重判断

public class Sluggard {private static Sluggard sInstance;private Sluggard() {}public synchronized static Sluggard getInstance() {if (sInstance == null) {synchronized (Sluggard.class) {if (sInstance == null) {sInstance = new Sluggard();}}}return sInstance;}
}

这个时候依然有问题,在字节码下是有问题的

字节码的步骤:

1.分配空间

2.初始化

3.引用赋值

但是这个2和3的顺序是可以变化的。

导致这里不是null确返回了null的问题。

解决方法volatile关键字,防止指令重排序。

饿汉式

public class HungrySingleton {private static HungrySingleton sHungry;private HungrySingleton(){}public static HungrySingleton getInstance() {return sHungry;}}

优点:

  1. 线程安全:在类被加载时就已经创建了唯一的实例,不需要考虑多线程下的同步问题。
  2. 实现简单:饿汉式实现单例模式比较简单,代码易于理解和维护。
  3. 延迟加载:因为饿汉式在类加载时就已经创建了实例,所以可以保证实例在首次被使用之前已经被创建出来,不需要等待。

缺点:

  1. 资源浪费:如果单例对象很大且长时间不被使用,那么饿汉式会浪费大量的系统资源,降低系统性能。

  2. 不能实现懒加载:饿汉式在类被加载时就已经创建了实例,无法实现延迟加载,即使单例对象在应用中从未被使用过,也会被创建出来。

  3. 在多线程环境下,如果一个线程在获取单例对象的同时,另一个线程正在初始化该单例对象,那么就可能会出现死锁。

    假设有两个线程 A 和 B,线程 A 先获取单例对象,但是单例对象还未初始化,此时线程 A 会在 getInstance() 方法中等待单例对象初始化完成,然后再返回单例对象。而线程 B 此时也想获取单例对象,但是线程 A 正在等待单例对象初始化完成,因此线程 B 会一直等待线程 A 释放锁,从而出现死锁。

因此,饿汉式适用于单例对象较小且经常被使用的情况。如果单例对象很大或者需要延迟加载,那么可以考虑使用懒汉式实现单例模式。

静态内部类

public class InnerClassSingleton {private InnerClassSingleton(){}public static class InnerClassSingletonHolder{private static InnerClassSingleton sInnerClassSingletonHolder=new InnerClassSingleton();}public static InnerClassSingleton getInstance() {return InnerClassSingletonHolder.sInnerClassSingletonHolder;}
}

优点:

  1. 线程安全:由于静态内部类只会被加载一次,因此线程安全得到了保证。
  2. 延迟加载:只有在调用 getInstance() 方法时,才会加载内部类,从而创建单例对象,达到了懒加载的目的。
  3. 实现简单:相比于饿汉式和双重校验锁,静态内部类实现单例模式更加简单。

缺点:

  1. 无法传递参数:静态内部类无法访问外部类的非静态成员变量和方法,也就无法在创建单例对象时传递参数。
  2. 无法防止反射攻击:和其他单例模式一样,静态内部类也无法防止反射攻击。

因此,静态内部类适用于单例对象较大或者需要延迟加载的情况,并且不需要在创建单例对象时传递参数。如果需要传递参数或者需要防止反射攻击,可以考虑使用枚举类型实现单例模式。

枚举类单例模式:

一种简单而安全的单例模式实现方式。在 Java 中,枚举类型是一种特殊的类,可以看做是单例的一种实现。使用枚举类实现单例模式可以避免线程安全问题,同时也避免了反射和序列化等问题。

public enum Singleton {INSTANCE;public void doSomething() {// do something}
}

在上面的示例代码中,枚举值 INSTANCE 就是单例对象。由于枚举类型是天然线程安全的,因此这种方式不需要考虑线程安全问题。同时,由于枚举类型没有构造函数,也没有对外暴露的构造函数或者静态方法,因此可以避免通过反射和序列化等方式来创建新的实例。

使用枚举类实现单例模式的优点包括:

  • 线程安全:枚举类型是天然线程安全的。
  • 简单易用:枚举类型的定义非常简单,使用起来也非常方便。
  • 序列化安全:枚举类型不允许通过反序列化来创建新的实例,因此也是序列化安全的。

缺点:

  • 不支持懒加载:枚举类在被加载时就会实例化对象,因此不能实现懒加载的效果。如果应用程序需要大量的单例对象,这种立即初始化的方式可能会导致启动时间延长或占用过多的内存。
  • 不支持继承:枚举类无法被继承,因此无法扩展它们的行为或在不同的环境中进行子类化。
  • 可读性不强:与其他单例模式相比,枚举类单例模式的代码可能更难理解,因为它们使用了一些Java编程中相对不常见的技巧。