单例设计模式

为什么要使用单例

普通的类就像生产机器,可以生产多个实例(动物糕点),就像下图:

单例设计模式(Singleton Design Pattern)对实例做限制,一个类只允许创建一个对象。

那么,对类进行限制有什么好处?假设对文件进行写入,同一个对的多个线程同时写入会造成资源竞争问题,比如两个线程同时给一个共享变量加 1 ,共享变量最后的结果可能只加了 1 。

如果是同一个对象,加入对象级别互斥锁就能解决问题。

如果是 Java 程序,可以用 synchronized 关键字给写入操作加入互斥锁:


public class Logger {

  private FileWriter writer;

  public Logger() {

    // 创建文件

    File file = new File("/Users/wangzheng/log.txt");

   // 进行追加写入

    writer = new FileWriter(file, true);

  }

  public void log(String message) {

    // 为 write 加入互斥锁

    synchronized(this) {

      writer.write(mesasge);

    }

  }

}

但是,这段代码存在两个问题:

  1. FileWriter 本身就是对象安全的,代码多此一举,。

  2. 这是对象锁,不同线程使用同一对象才有效,若使用不同对象,没有效(如果不理解,见下图)。

其实问题不难解决,把对象级别锁提升到类级别即可:


public class Logger {

  private FileWriter writer;

  public Logger() {

    File file = new File("/Users/wangzheng/log.txt");

    writer = new FileWriter(file, true);

  }

  public void log(String message) {

    // 使用类级别的锁

    synchronized(Logger.class) {

      writer.write(mesasge);

    }

  }

}

其实还有很多方案,分布式锁,并发队列等等,但这些方案都比较复杂,而单例模式的思路简单清晰。下面来实现多个单例。

饿汉式单例

顾名思义,饥饿难忍,在类加载时就创建好实例

很多语言都支持在类加载前就初始化变量的操作,比如 Java 的静态变量和 Python 的类变量,利用这个特性就能实现饿汉式单例。

java 实现


public class IdMaker {

    // Java 静态属性只会加载一次,所以 instance 只会被初始化一次

    // 利用 Java 静态属性,可以避免多线程资源竞争问题

    // 饿汉式:静态属性,在类加载阶段,完成初始化

    private static IdMaker instance = new IdMaker();

    // ID 计数器,默认值是 -1

    private int id = -1;

    // 把 IdMaker 的构造函数,设为私有(用于阻止调用者实例化 IdMaker)

    private IdMaker() {

    }

    // 获取实例

    public static IdMaker getInstance() {

        return instance;

    }

    // 通过 ++ 操作,获取不一样的 ID 值

    public int getId() {

        id++;

        return id;

    }

}

class TestIdMaker {

    public static void main(String[] args) {

        // 获取唯一的 ID

        int id1 = IdMaker.getInstance().getId();

        int id2 = IdMaker.getInstance().getId();

        int id3 = IdMaker.getInstance().getId();

        System.out.println(id1);

        System.out.println(id2);

        System.out.println(id3);

        // 0 1 2

    }

}

python


class IdMaker:

    # python 的类变量会被多个类,实例共享

    __instance = None

    # __id 也是类变量,多个实例或类共享

    __id = -1

    # python 在类加载阶段,通过父类的 __new__ 创建实例,如果我们重写 __new__

    # 就不会调用父类的 __new__ ,就会调用我们写的 __new__ 创建实例

    # __new__ 需要返回一个实例,如果不返回,就不会实例化

    def __new__(cls):

        if cls.__instance is None:

            # 父类的 __new__ ,参数接收一个类名,会返回类的实例

            cls.__instance = super().__new__(cls)

        return cls.__instance

    # 计数器,在获取前,进行 + 1

    def get_id(self):

        self.__id += 1

        return self.__id

def test_id_maker():

    # IdMaker 是单例类,只允许有一个实例

    id1 = IdMaker().get_id()

    id2 = IdMaker().get_id()

    id3 = IdMaker().get_id()

    print(id1, id2, id3)

if __name__ == "__main__":

    test_id_maker()

    # 0 1 2

因为过于饥饿,该实现不支持延迟加载(用到的时候再初始化),如果实例占用资源多会造成初始化的慢,卡。其实,将占用资源多的方法提前(饿汉),有利有弊:

  • 利:避免运行中卡顿,在初始化阶段就能发现错误

  • 弊:提前初始化浪费资源

懒汉式单例

懒汉非常懒惰,在类使用阶段才会创建实例

懒汉式可以弥补饿汉式缺点,由于在使用实例时才创建,避免初始化阶段卡慢。

Java 实现


public class IdMaker {

    // Java 引用类型的默认属性是 null

    // 在类加载阶段,不进行初始化

    private static IdMaker instance;

    // ID 计数器,默认值是 -1

    private int id = -1;

    // 把 IdMaker 的构造函数,设为私有(用于阻止调用者实例化 IdMaker)

    private IdMaker() {

    }

    // 懒汉式:在获取实例的阶段,进行初始化

    // synchronized 是互斥锁,为了保证在多线程时,只实例化一次

    public static synchronized IdMaker getInstance() {

        // 如果发现 instance 没有被初始化,就完成初始化

        if (instance == null)

            instance = new IdMaker();

        return instance;

    }

    // 通过 ++ 操作,获取不一样的 ID 值

    public int getId() {

        id++;

        return id;

    }

}

class TestIdMaker {

    public static void main(String[] args) {

        // 获取唯一的 ID

        int id1 = IdMaker.getInstance().getId();

        int id2 = IdMaker.getInstance().getId();

        int id3 = IdMaker.getInstance().getId();

        System.out.println(id1);

        System.out.println(id2);

        System.out.println(id3);

        // 0 1 2

    }

}

python 实现


from threading import Lock

class IdMaker:

    # 申请一个线程锁

    __instance_lock = Lock()

    # python 的类变量会被多个类,实例共享

    __instance = None

    # __id 也是类变量,多个实例或类共享

    __id = -1

    # 如果 __new__ 抛出异常,就不允许调用者进行实例化

    def __new__(cls):

        raise ImportError("Instantition not allowed")

    # 类方法不用实例化也能调用,因为我们不允许进行实例化,所以要使用类方法

    @classmethod

    def get_instance(cls):

        # with 会帮我们自动的上锁和释放,不用我们操心

        with cls.__instance_lock:

            if cls.__instance is None:

                # 因为我们的 __new__ 代码不允许进行实例化,所以可以借用父类的 __new__ 进行实例化

                cls.__instance = super().__new__(cls)

        return cls.__instance

    # 计数器,在获取前,进行 + 1

    def get_id(self):

        self.__id += 1

        return self.__id

def test_id_maker():

    # IdMaker 是单例类,只允许有一个实例

    id1 = IdMaker.get_instance().get_id()

    id2 = IdMaker.get_instance().get_id()

    id3 = IdMaker.get_instance().get_id()

    print(id1, id2, id3)

if __name__ == "__main__":

    test_id_maker()

    # 0 1 2