为什么要使用单例
普通的类就像生产机器,可以生产多个实例(动物糕点),就像下图:
单例设计模式(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);
}
}
}
但是,这段代码存在两个问题:
-
FileWriter 本身就是对象安全的,代码多此一举,。
-
这是对象锁,不同线程使用同一对象才有效,若使用不同对象,没有效(如果不理解,见下图)。
其实问题不难解决,把对象级别锁提升到类级别即可:
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