21.有些类也需要计划生育 - 单例模式 (Singleton pattern)

21.1 类也需要计划生育

  • 时间: 5月29日20点   地点: 小菜大鸟住所的客厅   人物: 大鸟, 小菜

     “大鸟, 今天我在公司写一个 MDI 窗体程序, 当中有一个是 ‘工具箱’ 的窗体, 问题就是, 我希望工具箱要目不出现, 出现也只出现一个, 可实际上却是我每次点击菜单, 实例化 ‘工具箱’, 它就会出来一个, 这样点击多次就会出现多个 ‘工具箱’, 你说怎么办?”

     “哈, 显然, 你这个 ‘工具箱’ 类需要计划生育啊, 你让他超生了, 当然是不好的.”
     “大鸟, 你又在说笑了, 不过计划生育的说法也算是贴切吧, 现在我就是希望他要么不要有, 有就只能有一个, 如何办?”
     “其实这就是一个设计模式的应用, 你先说说你是怎么写的?”
     ….. 这个模式实在是太熟悉了, 实在是懒得写背景故事了…

21.4 单例模式

单例模式(Singleton Pattern), 保证一个类仅有一个实例, 并提供一个访问他的全局访问点. [DP]

     通常我们可以让一个全局变量的一个对象被访问, 但它不能防止你实例化多个对象. 一个最好的办法就是, 让类自身负责保存它的唯一实例. 这个类可以保证没有其它实例可以被创建, 并且他可以提供一个访问该实例的方法.[DP]



     Singleton 类, 定义一个 getInstance() 操作, 允许客户访问他的唯一实例. getInstance() 是一个静态方法, 主要负责创建自己的唯一实例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton {
private static Singleton instance;
private Singleton() {
// 构造方法让其 private, 这就堵死了外界利用 new
// 创建此类实例的可能
}
// 此方法是获得次实力类的唯一全局访问点
public static Singleton getInstance() {
// 若实力类不存在, 则 new 一个新的实例
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

     “单例模式除了可以保证唯一的实例外, 还有什么好处呢?”
     “好处还是有的呀, 比如单例模式因为 Singleton 类封装它唯一的实例, 这样他可以严格的控制客户怎样访问它. 简单说就是对唯一实例的受控访问.
     “我怎么感觉单例模式有点像一个实用类的静态方法, 比如 .Net 框架里的 Math 类, 有很多数据计算方法, 这两者有什么区别呢?”
     “你说的没错, 他们之间的确很类似, 实用类通常也会采用私有化的构造方法来避免其有实例. 但他们还是有很多的不同, 比如实例类不用保存状态, 仅提供一些静态方法或静态属性让你使用, 而单例类是有状态的. 实用类不能用于继承多态, 而单例虽然实例唯一, 确实可以有子类来继承. 实用类只不过是一些方法的属性集合, 而单例却是有着唯一的对象实例. 在运用中还要仔细分析再做决定用哪一种方式.”
     “哦, 我明白了.”

21.5 多线程是的单例

     “另外, 你还需要注意一些细节, 比如说, 多线程程序中, 多个线程同时, 注意是同时访问 Singleton类, 调用getInstance()方法, 会有可能造成创建多个实例的.”
     “啊, 是呀, 这应该怎么办呢?”
     “可以给进程加一把锁来处理. 这里需要解释一下 synchronize 的语句含义, synchronize 是确保当一个线程位于代码的临界区时, 令一个线程不进入临界区. 如果其他线程试图进入锁定的代码, 则他将一直等待(即被阻止), 直到该对象被释放.[Think in java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {
private static Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
// 在同一时刻加了所的那部分程序,
// 只有一个线程可以进入
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}

     “这段代码是的对象实例有最先进入的那个线程创建, 以后的线程在进入时不会再去创建. 由于有了 synchronize, 就保证了多线程环境下的同时访问也不会造成多个实例的生成.”
     “为什么不直接 synchronize(instance), 而是使用 Singleton.class 呢?”
     “小菜呀, 加锁时, instance 实例有没有被创建还都不知道, 怎么对它加锁呢?”
     “我知道了, 原来是这样. 但是这就每次调用 getInstance() 方法是都需要 synchronize, 好像不太好吧.”
     “说得非常好, 的确是这样, 这种做法是会影响性能的, 所以对这个类还是要做改良.”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton {
private volatile static Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
// 先判断实例存不存在
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

     “现在这样, 我们不用让线程每次都加锁, 而是在实例未被创建的时候在加锁处理. 同时也能保证多线程的安全. 这种做法被称为 Double-Check Locking (双重锁定).”
     “我有问题, 我在外面已经判断了 instance 实例是否存在, 为什么在 synchronize 里还需要做一次 instance 实例是否存在的判断呢?”

1
2
3
4
5
6
7
8
9
10
11
public static Singleton getInstance() {
...
if (instance == null) { // 这个点
synchronized (Singleton.class) {
if (instance == null) { // 这个点
instance = new Singleton();
}
}
}
return instance;
}

     “那是因为你没有仔细分析. 对于instance 存在的情况, 就直接返回, 这没有问题. 当 instance 为 null, 并且同时有两个线程调用 getInstance() 方法是, 他们都可以通过第一重 (instance == null) 的判断. 然后又与 synchronize 机制, 这个两个线程只有一个进入, 另一个在外排队等候, 必须要其中一个进入并出来后, 另一个才能进入. 而此时如果没有了第二重的 (instance == null)判断, 则第一个线程创建了实例, 而第二个线程还是可以继续在创建新的实例, 这就没有达到单例的目的. 明白了吗?”
     “哦, 我明白了, 原来有这么麻烦呀.”

21.7 静态初始化

     “其实在实际应用当中,双重锁定模式已经可以满足需求了, 但是在高并发环境下, 双重锁定模式也是可能出问题的(JDK1.5 以前), 更推荐一种静态初始化的方式. (也被称为 Initialization Demand Holder | IoDH).”

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private Singleton() { }
// 让静态内部类来持有这个对象.
private static class HolderClass {
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return HolderClass.instance;
}
}

     “由于静态单例对象没有作为 Singleton 的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用 getInstance() 时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于 getInstance() 方法没有任何线程锁定,因此其性能不会造成任何影响”
     “通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式, 只不过对于不同的平台会有限制, 在C# 就是另一种语法了.”
     “没想到小小的单例模式也有这么多需要考虑的问题.” 小菜感慨道.

~感谢捧场,您的支持将鼓励我继续创作~