单例模式的线程安全问题

在例1中的ClassicSingleton.getInstance()方法由于下面的代码而不是线程安全的:
 if(instance == null)
 {
  instance = new Singleton();
 }


一种性能改进的方法

寻找一种性能改进方法时,你可能会选择像下面这样重写getInstance()方法:

 public static Singleton getInstance() {
  if (singleton == null) {
   synchronized (Singleton.class) {
    singleton = new Singleton();
   }
  }
  return singleton;
 }

这个代码片段只同步了关键的代码,而不是同步整个方法。然而这段代码却不是线程安全的。考虑一下下面的假定:线程1进入同步块,并且在它给singleton成员变量赋值之前线程1被切换。接着另一个线程进入if块。第二个线程将等待直到第一个线程完成,并且仍然会得到两个不同的单例类实例。有修复这个问题的方法吗?请读下去。

双重加锁检查

初看上去,双重加锁检查似乎是一种使懒汉式实例化为线程安全的技术。下面的代码片段展示了这种技术:

 public static Singleton getInstance() {
  if (singleton == null) {
   synchronized (Singleton.class) {
    if (singleton == null) {
     singleton = new Singleton();
    }
   }
  }
  return singleton;
 }
如果两个线程同时访问getInstance()方法会发生什么?想像一下线程1进行同步块马上又被切换。接着,第二个线程进入if 块。当线程1退出同步块时,线程2会重新检查看是否singleton实例仍然为null。因为线程1设置了singleton成员变量,所以线程2的第二次检查会失败,第二个单例类实例也就不会被创建。似乎就是如此。
不幸的是,双重加锁检查不会保证正常工作,因为编译器会在Singleton的构造方法被调用之前随意给singleton赋一个值。如果在singleton引用被赋值之后而被初始化之前线程1被切换,线程2就会被返回一个对未初始化的单例类实例的引用。

一个改进的线程安全的单例模式实现

例7列出了一个简单、快速而又是线程安全的单例模式实现:
例7.一个简单的单例类

public class Singleton {
 public final static Singleton INSTANCE = new Singleton();

 private Singleton() { // Exists
 }
}
这段代码是线程安全的是因为静态成员变量一定会在类被第一次访问时被创建。你得到了一个自动使用了懒汉式实例化的线程安全的实现;你应该这样使用它:

Singleton singleton = Singleton.INSTANCE; singleton.dothis(); singleton.dothat(); ...

当然万事并不完美,前面的Singleton只是一个折衷的方案;如果你使用那个实现,你就无法改变它以便后来你可能想要允许多个单例类的实例。用一种更折哀的单例模式实现(通过一个getInstance()方法获得实例)你可以改变这个方法以便返回一个唯一的实例或者是数百个实例中的一个.你不能用一个公开且是静态的(public static)成员变量这样做.

你可以安全的使用例7的单例模式实现或者是例1的带一个同步的getInstance()方法的实现.

 

---------------------------------------------------------------------------------------------------

 

实际上 看《Java设计模式 》一书8.2 单例和线程 你会发现那样写就不会有上面的问题了。

8.2  单例和线程

如果要在多线程环境中对单例采用滞后初始化,那么我们必须小心防止多个线程同时初始化该单例。在多线程环境中,我们无法保证一个方法能够持续运行到结束,其他线程的方法才开始运行。因而可能存在这样一种情形:两个线程几乎同时尝试初始化单例类。假设第一个方法发现单例为空,而第二个方法在此刻开始运行,它也会发现该单例为空。接下来,这两个方法都将会对该单例类进行初始化。为了防止这种类型的竞争,需要提供一个锁机制来协调不同线程中多个方法的运行。

Java语言及其类库为多线程开发提供了很好的支持。特别地,Java为每个对象提供一个锁(lock),它可以用于表示对象是否已被某个线程占用。若要确保只有一个线程可以初始化单例类,我们可以利用适当的对象锁来实现对单例初始化进行同步。其他方法,如需互斥访问单例的方法,也可以基于锁的方案进行同步。《Java并发编程》[Lea, 2000]一书建议使用属于当前类的锁进行同步。代码如下:

 

 

getFactory()的代码保证:在一个线程开始滞后初始化的时候,如果有另一个线程也准备开始初始化,这时候,第二个线程将停止执行,等待获取对象classLock的锁。当第二个线程获取这个锁并开始执行初始化的时候,它会发现该单例已不再为空(因为只存在该类的唯一实例,我们可以使用单个静态锁)。

wipMoves变量记录了半成品(WIP)已经完成的工序的数目。每当一个材料箱移到一台新机器上时,引发或记录材料箱移动的子系统必须调用工厂单例的recordWipMove()方法。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值