除了奋斗,你别无选择!

——————-此部分比较深入地讲解了单例模式,原文链接已给出,后续将涉及一些常见面试问题—————————

原文地址:

关于单例模式的文章,其实网上早就已经泛滥了。但一个小小的单例,里面却是有着许多的变化。网上的文章大多也是提到了其中的一个或几个点,很少有比较全面且脉络清晰的文章,于是,我便萌生了写这篇文章的念头。企图把这个单例说透,说深入。但愿我不会做的太差。

  首先来看一下简单的实现

/** * 基础的单例模式,Lazy模式,非线程安全 * 优点:lazy,初次使用时实例化单例,避免资源浪费 * 缺点:1、lazy,如果实例初始化非常耗时,初始使用时,可能造成性能问题 * 2、非线程安全。多线程下可能会有多个实例被初始化。 * * @author laichendong * @since 2011-12-5 */public class SingletonOne {/** 单例实例变量 */private static SingletonOne instance = null;/*** 私有化的构造方法,保证外部的类不能通过构造器来实例化。17*/private SingletonOne() {}/*** 获取单例对象实例** @return 单例对象26*/public static SingletonOne getInstance() {if (instance == null) { // 1instance = new SingletonOne(); // 2}return instance;}}

注释中已经有简单的分析了。接下来分析一下关于“非线程安全”的部分。

  1、当线程A进入到第28行(#1)时,检查instance是否为空,此时是空的。  2、此时,线程B也进入到28行(#1)。切换到线程B执行。同样检查instance为空,于是往下执行29行(#2),创建了一个实例。接着返回了。  3、在切换回线程A,由于之前检查到instance为空。所以也会执行29行(#2)创建实例。返回。  4、至此,已经有两个实例被创建了,这不是我们所希望的。

怎么解决线程安全问题?

  方法一:同步方法。即在getInstance()方法上加上synchronized关键字。这时单例变成了

使用同步方法的单例/** * copyright © sf-express Inc */package com.something.singleton;/** * 同步方法 的单例模式,Lazy模式,线程安全 * 优点: * 1、lazy,初次使用时实例化单例,避免资源浪费 * 2、线程安全 * 缺点: * 1、lazy,如果实例初始化非常耗时,初始使用时,可能造成性能问题 * 2、每次调用getInstance()都要获得同步锁,性能消耗。 * * @author laichendong * @since 2011-12-5 */public class SingletonTwo {/** 单例实例变量 */private static SingletonTwo instance = null;/*** 私有化的构造方法,保证外部的类不能通过构造器来实例化。*/private SingletonTwo() {}/*** 获取单例对象实例* 同步方法,实现线程互斥访问,保证线程安全。** @return 单例对象*/public static synchronized SingletonTwo getInstance() {if (instance == null) { // 1instance = new SingletonTwo(); // 2}return instance;}}

加上synchronized后确实实现了线程的互斥访问getInstance()方法。从而保证了线程安全。但是这样就完美了么?我们看。其实在典型实现里,会导致问题的只是当instance还没有被实例化的时候,多个线程访问#1的代码才会导致问题。而当instance已经实例化完成后。每次调用getInstance(),其实都是直接返回的。即使是多个线程访问,也不会出问题。但给方法加上synchronized后。所有getInstance()的调用都要同步了。其实我们只是在第一次调用的时候要同步。而同步需要消耗性能。这就是问题。

  方法二:双重检查加锁Double-checked locking。  其实经过分析发现,我们只要保证instance =newSingletonOne();是线程互斥访问的就可以保证线程安全了。那把同步方法加以改造,只用synchronized块包裹这一句。就得到了下面的代码:

public static SingletonThree getInstance() {if (instance == null) { // 1synchronized (SingletonThree.class) {instance = new SingletonThree(); // 2}}return instance;}

这个方法可行么?分析一下发现是不行的!  1、线程A和线程B同时进入//1的位置。这时instance是为空的。  2、线程A进入synchronized块,创建实例,线程B等待。  3、线程A返回,线程B继续进入synchronized块,创建实例。。。  4、这时已经有两个实例创建了。

  为了解决这个问题。我们需要在//2的之前,再加上一次检查instance是否被实例化。(双重检查加锁)接下来,代码变成了这样:

public static SingletonThree getInstance() {if (instance == null) { // 1synchronized (SingletonThree.class) {if (instance == null) {instance = new SingletonThree(); // 2}}}return instance;}

这样,当线程A返回,线程B进入synchronized块后,会先检查一下instance实例是否被创建,这时实例已经被线程A创建过了。所以线程B不会再创建实例,而是直接返回。貌似!到此为止,这个问题已经被我们完美的解决了。遗憾的是,事实完全不是这样!这个方法在单核和 多核的cpu下都不能保证很好的工作。导致这个方法失败的原因是当前java平台的内存模型。java平台内存模型中有一个叫“无序写”(out-of-order writes)的机制。正是这个机制导致了双重检查加锁方法的失效。这个问题的关键在上面代码上的第5行:instance =newSingletonThree();这行其实做了两个事情:1、调用构造方法,创建了一个实例。2、把这个实例赋值给instance这个实例变量。可问题就是,这两步jvm是不保证顺序的。也就是说。可能在调用构造方法之前,instance已经被设置为非空了。下面我们看一下出问题的过程:  1、线程A进入getInstance()方法。  2、因为此时instance为空,所以线程A进入synchronized块。  3、线程A执行instance =newSingletonThree();把实例变量instance设置成了非空。(注意,是在调用构造方法之前。)  4、线程A退出,线程B进入。  5、线程B检查instance是否为空,此时不为空(第三步的时候被线程A设置成了非空)。线程B返回instance的引用。(问题出现了,这时instance的引用并不是SingletonThree的实例,因为没有调用构造方法。)  6、线程B退出,线程A进入。  7、线程A继续调用构造方法,完成instance的初始化,再返回。

人,也总是很难发现自己的错误,

除了奋斗,你别无选择!

相关文章:

你感兴趣的文章:

标签云: