单例模式的实现

当谈及单例模式时,我们指的是一种设计模式,它确保某个类只有一个实例,并提供一个全局访问点。在Java中,单例模式是最常用的设计模式之一,它可以确保一个类在应用程序的生命周期内只有一个实例,并提供全局访问点以便访问该实例。

1. 为什么要使用单例模式?

单例模式主要用于以下几种情况:

  1. 当一个类只能有一个实例,且客户端需要访问这个实例时,可以使用单例模式。
  2. 当希望避免由于创建太多对象导致的性能问题时,可以使用单例模式。
  3. 当需要控制资源的访问权限时,可以使用单例模式。

2. 实现单例模式的方式

在Java中,有多种方式可以实现单例模式,下面我们将介绍其中比较常见的三种实现方式:懒汉式、饿汉式和双重检查锁定。

2.1 饿汉模式

饿汉模式是指在类加载时就创建实例,由于这个实例创建的非常早,所以使用饿汉描述,形容非常迫切。这种方式在多线程环境下也能保证单例的唯一性。

 示例代码:

class Singleton1 {
    private static Singleton1 instance = new Singleton1();

    public static Singleton1 getInstance() {
        return instance;
    }

    private Singleton1() {};
}

在上述代码中我们可以看到,我们在类属性中实例化了一个该类的对象,然后给出了一个获取这个对象的方法,然后我们把默认的无参构造方法实现为 “私有的”,让外界无法直接实例化新的对象。

2.2懒汉模式

懒汉式单例是指在需要时才创建实例。当第一次调用获取实例的方法时,才会创建实例对象。

示例代码:

class SingletonLazy {
    private static SingletonLazy instance = null;
    private SingletonLazy() {}
    
    public static SingletonLazy getInstance() {
        if(instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

如果首次调用getInstance() ,此时instance为null,就会进入if条件,从而创建实例,等再次调用getInstance则不会再创建实例。

但是上如代码如果在多线程中调用,是有可能创建多个实例的:

假设有两个线程 t1, 和 t2 都在调用 getInstance ,当 t1 刚好执行到进入 if 内部 还没有 把创建好的实例赋值给 instance 的时候,被调度除了cpu,此时,instance 仍然等于 null, t2线程就有可能进入 if 内从而再次创建一个实例。

如何改进,让上述代码变为线程安全的代码?

上诉代码线程不安全的原因是,if 和 new 操作之间可能再次运行了其他线程的 if 和 new 操作,于是我们可以通过加锁,把这两个操作打包在一起:

class SingletonLazy {
    private static SingletonLazy instance = null;
    private SingletonLazy() {}

    private static Object locker = new Object();
    public static SingletonLazy getInstance() {
        synchronized (locker) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
}

上述代码就保证了 if 和 new 操作之间 不会执行其他线程的 if 和 new 操作。但是我们发现上述代码,其实只有在第一次调用的时候才需要加锁,后续都直接返回 instance 即可 ,但是每次仍然都是先加锁再解锁,这样会导致该代码的效率会变低,于是我们可以在该方法的上面再加一个 if 判断:

class SingletonLazy {
    private static SingletonLazy instance = null;
    private SingletonLazy() {}

    private static Object locker = new Object();
    public static SingletonLazy getInstance() {
        if(instance == null) {
            //如果 instance 为 null 说明是首次调用,需要加锁
            synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

注意上述代码中两个 if 的结果是可能不同的。第一个 if 和 第二个 if 之间可能 会执行其他 线程调用 的 getInstance 导致 instance 不为 null。

上述代码 虽然保证了 if 和 new 操作之间 不会执行其他线程的 if 和 new 操作,但仍可能存在线程安全问题。

指令重排序引起的线程安全问题:

指令重排序是指处理器在执行指令时可能会改变指令的顺序,以提高程序的运行效率。

我们来看这行代码:

instance = new SingletonLazy();

这行代码可以拆分为三个步骤(不是三个cpu指令):

  1. 申请一段内存空间
  2. 在这个内存上创建出实例
  3. 把这段空间的内存地址赋值给 instance 

正常情况下,上述步骤是按 1 2 3 的顺序来执行的,但是编译器也可能优化成 1 3 2来执行,
现在假设有 t1 , t2 两个线程 ,如果此时,t1 正在以 1 3 2,的顺序在执行这行代码,t1在刚执行了  1 和 3 之后 ,就被调度出了cpu ,此时,instance 的值已经不为空,但是指向的内存中却还没有创建 实例,此时 t2 线程又刚好开始执行 getInstance ,于是,t2 线程就得到了,一块没有被初始化的内存空间,此时如果,t2 再尝试使用这块内存空间中的内容,就会引发错误 此时的instance是一个“全0”的值。

所以我们 应该给 instance 加上 volatile 关键字, 来禁止指令重排序

class SingletonLazy {
     private volatile static SingletonLazy instance = null;
    private SingletonLazy() {}

    private static Object locker = new Object();
    public static SingletonLazy getInstance() {
        if(instance == null) {
            //如果 instance 为 null 说明是首次调用,需要加锁
            synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

 现在上述代码就是一个线程安全的代码了

2.3 懒汉模式和饿汉模式的优缺点

懒汉式单例模式: 优点:

  1. 延迟加载:只有在首次调用getInstance()方法时才会创建实例,可以节约资源。
  2. 线程安全(双重检查锁定):通过使用synchronized关键字和双重检查锁定机制,可以保证多线程环境下的线程安全性。

缺点:

  1. 可能存在线程安全问题:尽管在代码中通过使用synchronized关键字和双重检查锁定机制来解决线程安全性问题,但在某些特定情况下可能会发生失效的情况,例如编译器优化问题。
  2. 实现相对复杂:双重检查锁定机制的实现相对复杂,容易出错,并且对于不熟悉该机制的开发人员来说,难以理解和维护。

饿汉式单例模式: 优点:

  1. 简单直观:实现起来简单,不存在线程安全问题。
  2. 线程安全:由于实例在类加载时就创建,因此不存在多线程环境下的线程安全问题。

缺点:

  1. 资源浪费:在类加载时就创建实例对象,可能会导致资源的浪费,特别是在实例的创建过程中需要执行耗时操作或占用大量资源的情况下。
  2. 无法实现延迟加载:由于实例在类加载时就被创建,无法实现按需创建实例的延迟加载需求。

总结: 懒汉式单例模式具有延迟加载和线程安全的优点,但也存在一些线程安全问题和实现复杂的缺点。饿汉式单例模式简单直观,线程安全,但可能存在资源浪费和无法实现延迟加载的缺点。在选择单例模式实现方式时,需要根据具体需求和场景进行权衡和选择。

你可能感兴趣的:(单例模式,java,c语言)