Table of Contents
导言:多轨工程应用中的Singleton模式
Singleton模式是软件工程中最广泛使用的创建性设计模式之一。 它确保类只有一个实例,并提供该实例的全球访问点。在单线化的应用程序中,实施单线的简单简单:使构造器成为私有,提供一种静态方法,以返回一个急切或懒惰创建的单一实例。然而,在多线化的工程应用程序中,如嵌入式系统、高频交易平台、实时控制系统以及分布式数据库,问题变得更加复杂。线程安全、记忆可见度和性能限制需要仔细设计。单线的实现失误可能导致竞相条件、多重实例创建、僵局或微妙的缺陷,而这些缺陷众所周知难以复制和调试。
本文章考察了开发者在多条环境中执行Singleton模式时最常见的错误,解释了其中的深层原因,并提供了一套全面的最佳做法和模式来避免它们,其中还包括Java中实用的代码示例,在C++和C#中引用了等效模式,并建议外部资源供进一步阅读.
单子岛执行中的常见错误
即使是有经验的开发者在同时执行系统时也可能陷入陷阱。 下面是最常见的错误,每个错误都附有为什么它们很危险的解释。
1. 不使建筑师私自经营
任何单子的基部都是防止外部即时化的私人构造器。如果构造器是可访问的(公共、保护或包-私有),任何线程都可以创建新实例,打破单子化合同。在多线性代码中,当一个类被重构,而构造器的能见度被意外改变,或者类被子化(尽管一般不鼓励子化)时,这种情况可能会无意中发生。 总是要声明构造器是私有的,如果必须支持子类(rare),则使用一个保护的构造器,并记录预期的行为。
2. 处理线索安全失败
在单条化的环境中,简单的懒惰初始化效果很好:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
但在多线程的应用程序中,有两个或两个以上的线程可以在任何线程创建实例之前同时输入检查。然后每个线程开始创建自己的对象,违反了模式。这是经典的种族条件[,导致多个实例,并可能导致状态或资源泄露不一致。
3. 使用懒惰初始化,不进行适当的同步
即使开发者认识到线程安全的必要性,也常常会天真地添加同步。例如,同步整个方法有效,但引入了性能瓶颈:
public static synchronized Singleton getInstance() { ... }
每次呼叫都会获得并释放锁,即使在实例已经创建之后。在高意向的情景中,这种高通向可以严重降低吞吐量。更好的方法是使用[]双检查锁 [(下文讨论),但即使这种模式如果执行不当,也会有陷阱。
4. 同步过度使用
同步有多种形式:方法、块、、等等。超同步——在有精细控制时应用粗细的锁——导致不必要的争论。在一些工程应用(例如,具有严格延迟预算的实时系统)中,甚至几百纳秒的锁顶管理都是不可接受的。目标是在确保线条安全的同时,尽量减少关键部分。
5. 忽略波动 关键词
在 Java, C#, 和 C++ (用 [[FLT: 10]] ) 等语言中, [[FLT: 0]] 挥发式 [[FLT: 1] 关键词(或等值 ) 对于在多线程代码中正确可见至关重要。 如果没有它, 编译器或CPU 可能会重新排序指令, 而用一个线程所做的修改可能无法被另一个线程所看到。 在双检查的锁定模式中, 未宣布单顿实例为 [[FLT: 11]] 会导致线程看到一个部分构造的对象, 导致无法预测的行为。 这是最微妙和危险的错误之一 。
线条-安全单子执行的最佳做法
为了避免这些陷阱,遵循这些已经证明的战略。 每一种方法都涉及线性安全、性能和简单。
私人建筑和静态实例
无论初始化策略如何, 构造器必须是私有的。 单子实例应该存储在一个静态字段。 不要以任何方式暴露构造器, 并考虑在 Java (或 [[FLT: 13]]) 在 C# 中将类制成子分类 。
仅在必要的时候使用同步块
对于懒惰初始化,双检查的锁定模式会减少同步的间接费用:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
Singleton result = instance; // Local variable for performance
if (result == null) {
synchronized (Singleton.class) {
result = instance;
if (result == null) {
instance = result = new Singleton();
}
}
}
return result;
}
}
在此代码中, 同步块外的检查 [[ FLT: 1 15] ] 避免了实例已经存在的锁顶。 内部检查确保只创建一个线索。 关键词 [ [ [FLT: 16]] 阻止指令重排, 并确保任务完全可以被其他线索所看到。 请注意, 我们缓存实例为性能在本地变量中。 这个模式在 Java 5+( 适当的内存模型) 中是正确的, 并且类似 C# 和 C++( 使用 [ [ [ FLT: 18] ) , 并使用内存顺序 。
Eager 初始化( 初始化 )
如果总是需要单子, 并且创造成本低廉, 热心的初始化是最简单的线性安全方法:
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
类加载由JVM内在同步,所以不需要额外的协调。然而,这在类加载时间创造了实例,在资源约束的系统中,或者单吨依赖于尚未可用的运行时间配置时,这可能不可取。
静态持有者模式(按需启动)
这种模式将懒惰初始化与线程安全结合,而不明确同步:
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
类只有在首先被叫作]时才加载,JVM保证在类装时安全公布静态场,这被广泛视为Java单子最优雅的解决方案.
以Enum为基础的单子( Java)
Joshua Bloch的 有效的Java建议使用一个音符:
public enum Singleton {
INSTANCE;
// methods and fields
}
Enum常数是隐含的],Java语言保证enum事件只创建一次,即使在串行或反射攻击下也是如此,这既安全又简洁,不过,enums不能扩展类(只执行接口),所以它们不适合于所有使用例.
C++和C#中的等效模式
在C++中,迈尔的Singleton[(局部静态初始化)自C++11起是线性安全的:
Singleton& getInstance() {
static Singleton instance;
return instance;
}
C#中,类提供了内置的线性安全懒惰化:
public class Singleton {
private static readonly Lazy<Singleton> _lazy =
new Lazy<Singleton>(() => new Singleton());
public static Singleton Instance => _lazy.Value;
}
工程应用中的测试和考虑
在工程应用中,单子顿模式常常管理硬件驱动程序,配置设置,线程池,或日志服务等共享资源. 在多条测试中测试此类单子顿需要仔细设计. 考虑如下:
- 通过提供一种仅用于测试的受保护方法(例如])或通过界面注入依赖性的方法,使单顿可测试 。 许多现代应用完全避免单顿,而倾向于管理生命周期的依赖性注射框架。
- 实时或高频系统中的绩效剖面分析[:测量同步的间接费用。在某些情况下,使用[(C#)或(C++)的无锁单子可能是有正当理由的。
- 分布式系统要求单子是每个过程独有的,而不是跨过程的. 如果您需要一个全集群单子,请使用外部协调(例如数据库,动物园保存者,或领袖选举).
- 引用和序列化可以打破单顿. 在Java序列化中使用[],如果已经设定],则通过在构造器中抛出例外来防止反射即时化.
结论
单子图案仍然是软件工程师工具箱中有价值的工具,但是在多线性环境中实施它需要认真关注细节。 通过理解和避免常见的错误 — — 如非私人构造器、缺失同步、不适当的挥发性使用和过度同步 — — 开发者可以产生强力的高性能单子。 双子检查锁定模式、静态持有器模式和以铝为基础的单子在Java中都提供了安全和效率的坚实平衡。 在C++和C#中,现代语言特征进一步简化了任务。
进一步研究请参考以下资源:
最终,最好的单子执行是最简单的一个执行。 当出现疑问时,更喜欢急切的初始化或静态持有器模式,并总是写同时进行的单元测试,以验证争议中的正确性。