单例设计模式

单例设计模式

单例模式就是要确保类在内存中只有一个对象,该实例必须自动创建,并且对外提供;
在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能

把构造方法私有
在成员位置自己创建一个对象
通过一个公共的方法提供访问

什么情况下使用

单例设计模式通常在以下情况下需要使用:

  1. 当系统中某个类只需要一个实例存在时,可以使用单例模式确保只有一个实例被创建
  2. 当需要全局访问某个对象时,可以使用单例模式提供一个全局访问点
  3. 当希望控制某个类的实例个数,避免多次创建实例时,可以使用单例模式。
  4. 当需要节省系统资源,避免频繁创建和销毁对象时,可以使用单例模式。

总的来说,单例设计模式适用于需要确保只有一个实例存在提供全局访问点的情况下。

一般使用步骤:

  1. 创建一个类,并私有化构造函数 (控制类的实例化,确保类只能在其所在的类或伴生对象内部被实例化),并定义一个私有的静态内部类 companion object (伴生对象),用于实现单例模式。

  2. 在companion object中定义一个私有的静态变量 (instance),用于存储单例对象。将该变量的初始值设置为 null 。

  3. 定义一个公共的静态方法 (getInstance),用于获取单例对象。

在该方法中,检查instance变量是否为null。
如果为 null (即还没有被初始化),则在一个 synchronized 块中再次检查 instance 是否为 null。
如果仍然为null,则创建一个新的 UserManager 实例并将其赋值给 instance。
如果 instance 不为 null,则直接返回该实例。


synchronized —> 这是因为多线程环境下,可能同时有多个线程尝试创建instance,而synchronized(this)就保证了在同一时间只有一个线程可以执行if(instance == null)后面的代码。
最后,return instance!!得到对象。

synchronized:涉及线程之间的安全问题(多个对象去抢夺这一个对象的时候可能会出现线程不安全问题)
如果 instance 没有就去创建,创建过程中加把锁,先别急着访问,先把这个对象创建好了再来访问,进来之后再来判断一次
(有可能刚刚进来的时候刚好已经创建好了,创好了就没有必要去再做一次了),即将要创建了还是空则去创建这个对象
( 当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。

在 Java 中,关键字 synchronized 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到 synchronized 另外一个重要的作用, synchronized 可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代 Volatile 功能),这点确实也是很重要的。)

在Kotlin中,伴生对象(Companion Object)是一个在类内部定义的对象,它与类的实例无关,类似于Java中的静态成员。(可以实现类似静态类的功能)伴生对象在Kotlin中有以下几个重要的作用:

  1. 替代静态成员:Kotlin中没有静态成员的概念,可以使用伴生对象来模拟静态成员,包括静态属性和静态方法。
  2. 访问私有构造函数:伴生对象可以访问类的私有构造函数,因此可以在伴生对象中实现单例模式,保证类只有一个实例。
  3. 实现工厂方法:伴生对象可以包含工厂方法,用于创建类的实例,而不需要直接调用构造函数。
  4. 实现接口:伴生对象可以实现接口,使得类可以通过伴生对象来实现特定的接口方法。

总的来说,伴生对象在Kotlin中是一个非常灵活和有用的特性,可以帮助我们更好地组织代码结构,实现单例模式、工厂方法等设计模式,以及提供静态成员的功能。
companion object 是一个在类内部定义的对象,可以用来存储类的静态变量和方法。
在Android开发中,companion object经常被用来创建工具类或者管理静态资源。
通过companion object,可以在不创建类的实例的情况下访问类的静态成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FileManager private constructor(){

companion object{
private var instance:FileManager?= null

fun sharedInstance():FileManager{ //只有调用这个方法的时候才去创建这个对象
if (instance == null){
synchronized(this){ //每一个类、每个对象都有一把锁,
if (instance == null){
instance = FileManager()
}
}
}
return instance!!
}

}

}

在Java中没有类似Kotlin的伴生对象的概念。伴生对象是Kotlin中的特性,用来模拟静态方法和静态变量。在Java中,可以直接使用静态方法和静态变量来实现相同的功能,无需额外的伴生对象。

双重检查锁定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FileManager {

private static FileManager instance = null;

private FileManager() {
}

public static FileManager sharedInstance() {
if (instance == null) {

synchronized (FileManager.class) {
if (instance == null) {
instance = new FileManager();
}
}

}
return 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
class SPUtils private constructor() {
private lateinit var weakContext:WeakReference<Context>

private val sp:SharedPreferences by lazy {
weakContext.get()!!.getSharedPreferences(Constants.SharedFileName,Context.MODE_PRIVATE)
}

companion object{
private var instance:SPUtils? = null

fun defaultSPUtils(context: Context):SPUtils{
return instance?:SPUtils().also {
instance = it
it.weakContext = WeakReference(context)
}
}
}

var isFirst:Boolean = true
//写入数据
set(value) {
field = isFirst
sp.edit().also {
it.putBoolean(Constants.isFirstKey,value)
it.apply()
}
}
//读取数据
get() {
return sp.getBoolean(Constants.isFirstKey,true)
}
}

为什么要使用单例模式

单例设计模式是一种创建型设计模式,其核心思想是确保一个类只有一个实例,并提供一个全局访问点。使用单例设计模式有以下主要原因:

  1. 延迟初始化 :单例模式的另一个优点是可以延迟初始化。也就是说,只有在需要使用该实例时,才会创建该实例。这有助于节省系统资源,特别是在某些初始化成本较高的类上。
  2. 资源共享 :单例模式可以确保某个类只有一个实例,这对于需要大量计算资源或者占用大量内存的类特别有用。通过单例模式,我们可以实现资源的共享和重复利用,提高程序的效率。
  3. 全局访问点 :单例模式提供了一个全局的访问点,使得任何代码都可以方便地访问到这个唯一的实例。这有助于简化代码,并提高代码的可维护性。
  4. 线程安全性 :虽然单例模式的线程安全性需要额外注意,但通过适当的实现,单例模式也可以是线程安全的。线程安全的单例模式可以确保在并发环境下,仍然只创建一个单例实例。
  5. 易于扩展 :单例模式的实现往往依赖于静态变量和静态方法,这使得其代码结构简单且易于理解。当需要添加新的功能时,只需要修改一个地方即可。
  6. 总的来说,单例设计模式在处理那些需要只有一个实例的类时非常有用,它提供了一种简单、灵活的方式来保证一个类只有一个实例,并且这个实例可以被全局访问

面试总结

  • 单例模式(Singleton Pattern)是一种设计模式,用于确保一个类只有一个实例,并提供一个全局访问点。根据实例化的时机,单例模式可以分为 懒汉式饿汉式。(并且自行实例化并向整个系统提供这个实例)

    单例模式特征:

    1. 构造方法不对外开发的,一般是private
    2. 通过一个静态方法或者枚举返回单例类的对象
    3. 注意多线程的场景
    4. 注意单例类对象在反序列化时不会重新创建对象

1. 懒汉式单例

懒汉式单例的特点是 延迟加载,即在第一次使用时才创建实例。这种方式可以节省资源,但需要考虑线程安全问题。(调用时才创建)

特点:

  • 延迟加载:实例在第一次调用 getInstance() 时创建。
  • 线程安全:通过 synchronized 关键字确保线程安全,但会带来性能开销。
    • 判断等于null的时候就会去创建,但是去new的时候,还没等new完,另外一个线程也进来了,判断也是null,又进行创建了
  • 缺点:每次调用 getInstance() 都需要同步,影响性能。
    • 同步锁粒度太大了,影响性能
      • 整个 getInstance() 方法都是同步的。这意味着无论 instance 是否为 null,每次调用 getInstance() 方法时,都会对整个方法进行加锁。
      • 由于每次调用 getInstance() 时都需要获取锁,这会导致性能下降。只有在第一次调用时,实例才会被创建,之后的调用都只是在获取已创建的实例。每次方法调用都进行加锁,导致多线程环境下的性能下降。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class LazySingleton {
// 1. 私有静态变量,用于保存唯一实例
private static LazySingleton instance;

// 2. 私有构造函数,防止外部直接创建实例
private LazySingleton() {
// 初始化操作
}

// 3. 公共静态方法,提供全局访问点(synchronized线程安全的)
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}

// 其他方法
public void showMessage() {
System.out.println("Hello, I am a Lazy Singleton!");
}
}

1.1 懒汉式单例的优化(双重检查锁定DCL)

为了减少懒汉式单例的性能开销,可以使用 双重检查锁定(Double-Checked Locking)来优化。

特点:

  • 延迟加载:实例在第一次调用 getInstance() 时创建。
  • 线程安全:通过双重检查锁定确保线程安全,同时减少同步开销。
  • 性能优化:只有在实例未创建时才进行同步,避免每次调用都加锁。

代码实现:

使用volatile确保可见性有序性

private static volatile LazySingleton instance;
使用volatile可以禁止指令重排,使双重检查更加安全

instance = new LazySingleton();new对象的时候会做三件事情

  • instance 实例分配对象
  • 调用 LazySingleton 构造方法 初始化成员字段
  • LazySingleton 对象赋值给 instance

这三个步骤在jdk/jvm里面是乱序的,这三条指令会进行指令重排,

这样就不能保证第2步就一定是在第3步之前执行的,

所以DCL有可能失效(面试非常常考)

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
public class LazySingleton {
// 1. 私有静态变量,使用 volatile 确保可见性
private static volatile LazySingleton instance;

// 2. 私有构造函数,防止外部直接创建实例
private LazySingleton() {
// 初始化操作
}

// 3. 公共静态方法,提供全局访问点
public static LazySingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (LazySingleton.class) { // 加锁(锁的是整个对象)
if (instance == null) { // 第二次检查
instance = new LazySingleton();
}
}
}
return instance;
}

// 其他方法
public void showMessage() {
System.out.println("Hello, I am an Optimized Lazy Singleton!");
}
}

2. 饿汉式单例

饿汉式单例的特点是 立即加载,即在类加载时就创建实例。这种方式简单且线程安全,但可能会浪费资源。(自身就是线程安全的)(类加载时【初始化类的时候】立即创建实例)

特点:

  • 立即加载:实例在类加载时创建,无论是否使用。
  • 线程安全:由于实例在类加载时创建,天然线程安全。
  • 缺点:如果实例未被使用,可能会浪费内存资源。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class EagerSingleton {
// 1. 私有静态变量,类加载时立即创建实例
private static final EagerSingleton instance = new EagerSingleton();

// 2. 私有构造函数,防止外部直接创建实例
private EagerSingleton() {
// 初始化操作
}

// 3. 公共静态方法,提供全局访问点
public static EagerSingleton getInstance() {
return instance;
}

// 其他方法
public void showMessage() {
System.out.println("Hello, I am an Eager Singleton!");
}
}

3.静态内部类单例

是线程安全的,还不需要使用同步

当虚拟机第一次加载这个类的时候,内部类的instance并不会被初始化,只有当调用getInstance这个静态方法,才会导致内部类的加载,也可以做到延迟加载(懒汉式也是可以延迟加载)

既安全,又延迟加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Singleton {
// 1. 私有构造函数,防止外部直接创建实例
private Singleton() {
// 初始化操作
}

// 2. 静态内部类,持有单例实例
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

// 3. 公共静态方法,提供全局访问点
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}

// 其他方法
public void showMessage() {
System.out.println("Hello, I am a Static Inner Class Singleton!");
}
}

4.枚举单例模式

枚举默认是线程安全的

枚举单例模式的特点:

天然单例——枚举实例在 JVM 中是唯一的,且只会被初始化一次。无需手动实现单例逻辑。

线程安全——枚举的实例化由 JVM 保证线程安全,无需额外同步机制。

防止反射攻击——枚举类型不允许通过反射创建实例,避免了反射破坏单例。

防止序列化破坏——枚举类型的序列化和反序列化由 JVM 保证唯一性,避免了序列化破坏单例。

代码简洁——实现简单,无需复杂的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum EnumSingleton {
INSTANCE; // 单例实例

// 可以添加方法和属性
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

// 示例方法
public void showMessage() {
System.out.println("Hello, I am an Enum Singleton!");
}
}

5.容器式单例

  • 集中管理:通过容器管理多个单例实例。
  • 灵活性:可以根据需要动态创建和管理单例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.HashMap;
import java.util.Map;

public class ContainerSingleton {
private static Map<String, Object> instanceMap = new HashMap<>();

private ContainerSingleton() {}

public static void registerInstance(String key, Object instance) {
if (!instanceMap.containsKey(key)) {
instanceMap.put(key, instance);
}
}

public static Object getInstance(String key) {
return instanceMap.get(key);
}
}
Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2023-2025 Annie
  • Visitors: | Views:

嘿嘿 请我吃小蛋糕吧~

支付宝
微信