class Foo { 
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) {
      Helper h;
      synchronized(this) {
        h = helper;
        if (h == null) 
            synchronized (this) {
              h = new Helper();
            } 
        helper = h;
        } 
      }    
    return helper;
    }
  }上面这个类的锁是在何处释放的,这样的类释放线程安全。

解决方案 »

  1.   

    呵呵,你这个问题是经典的双重检查成例问题(Double check idiom),Double check idiom在java里面是行不通的,原因解释起来有些复杂,牵涉到底层的机制,网上大把资料,自己去查吧.如果资料看不懂的话,就再发贴问,到时再看能不能帮到你.
      

  2.   

    synchronized(this) { //获得this锁
            h = helper;
            if (h == null) 
                synchronized (this) {//自己已经获得了锁,为什么还要再次获得? 
                  h = new Helper();
                } 
            helper = h;
            } 
          }    
      

  3.   

    这样的代码确实没有什么实际意义。但上面那位所谓行不通的观点不知道是什么意思。还有那些名词俺也看不懂。写两个简单的例子也许能让大家更容易理解楼主的问题
    1、
            synchronized(this)
            {
                synchronized(this)
                {
                    synchronized(this)
                    {
                        
                    }
                }
            }
    这样的代码确实没什么用,但java里也不禁止,同时编译后也没有做任何优化或变动。用反编译工具看一下就知道了。
    2、
    public Helper newHelper{
        synchronized(this);
        {
            return new Helper();
        }
    }
    public Helper getHelper() {
        if (helper == null) {
          Helper h;
          synchronized(this) {
            h = helper;
            if (h == null) 
                h = newHelper();
            helper = h;
            } 
          }    
        return helper;
        }这段代码把new Helper那段代码重新写成了一个函数。效果跟楼主的代码是一样的。但大家看起来就不会觉得那么奇怪了。
      

  4.   

    1.正如楼上所说,这样的代码是没有意义的。
    2.关于双重检查成例,你不知道那是你的问题。我不知道楼主从何处copy过来的代码,但这是一个双重检查成例的fix版本,被人拿来做例子讲的,一字不差。
    3。双重检查成例的目的不是用来验证synchronized的机制的,它是C++中常用到的一个方法,作用相当与一个singleton,而在java中执行这样的代码却不能达到这样的目的,尽管代码从逻辑上看似乎行得通。这个是与java的内存模型与编译器原理有关。还是那句话,不知道就先去查一下。
      

  5.   

    楼主的问题首先是“上面这个类的锁是在何处释放的”。也许楼主正在学习和研究多线程模式,想了解这段代码的运行过程。当然,这段代码有错误,楼主就是想研究错在哪里。说了那么多跑题的话,或者告诉楼主“这段代码是错误的”,无助于楼主解决问题。我不知道的东西多了,谢谢楼上的提醒。关于java和双重检查成例,我也查了一下资料。很抱歉,还是对这个名词不感兴趣。况且,那个文章中关于某段例子代码的解释还有待商榷。说了一大堆比问题本身还复杂得多的东西,颇有卖弄的嫌疑。
      

  6.   

    2、
    public Helper newHelper{
        synchronized(this);
        {
            return new Helper();
        }
    }
    public Helper getHelper() {
        if (helper == null) {
          Helper h;
          synchronized(this) {
            h = helper;
            if (h == null) 
                h = newHelper();
            helper = h;
            } 
          }    
        return helper;
        }这段代码你觉得锁在哪释放?
    h = newHelper();执行完后?
    还是
    helper = h;
            } 之后?
      

  7.   

    呵呵,我可不认为一个在网上可以找到很多资料的问题有拿出来卖弄的必要...1.我之所以说出这个问题,是因为我觉得向楼主推荐照这个方向研究才是正确的,如果楼主或者其他看到的人有心的话,去搜索这个问题后不但会找到对这段代码的解释,而且会获得更多的知识,我可不认为这是件坏事.2.如果照你所说,只是解释一下这段代码中synchronized的部分,我觉得是不够的,因为就算楼主理解了synchronized的机制而没有发现其他的问题,然后把这段代码拿去使用,那就错了,因为这段代码达不到它想达到的目的.3.很不幸,你对这段代码的解释不太正确,你改动的代码跟原本的代码在本质上已经不同了,你的代码中两次synchronized的monitor已经不是同一个,根本就解释不到楼主原本的问题,仔细看看吧.4."说了一大堆比问题本身还复杂得多的东西",这个是我最不能同意的,首先楼主的问题并不简单,这也正是你会解释错误的原因,另外,我实在不明白你为什么会说"很抱歉,还是对这个名词不感兴趣",这不是一个名词的问题,这是学习java中的一个知识点,如果你不知道,那就会犯错, 而不是你所说的感不感兴趣的问题.5.简单解释一下,详细的还是希望大家查资料,也真心希望能指出我的错误,因为大家来到这里都是为了学习.
    这段代码的目的,其实是想实现一个singleton,在helper实例化之后就不再去创建一个新的,然而因为在java中修改h的reference和初始化Helper类的顺序并不确定,也就是说有可能h已经有了一个non-null的reference,但是new Helper()可能并未完成,这时候如果第二个线程进来,看到helper已经不是null,就返回了,而拿到的helper并没有初始化成功.
    可能会有人问"help = h"这句话是在"synchronized(this){h = new Helper();}"之后发生的,那么就应该是new Helper()完了之后,才会将help的reference改变啊,这个就是synchronized机制的问题了,因为java中用synchronized的时候,当你获得一个锁,那么在你释放这个锁之前,synchronized中所有的action都要做完,而代码中内部的那个synchronized用的是和外部那个同一个锁,这会使java的编译器把"helper = h"的位置提前,以保证所有action在锁被释放之前发生.解决这个问题的一个方法是直接将getHelper()这个方法synchronized,当然这样一来会有一些性能的损失.
      

  8.   

    这样看来,内部那个synchronized根本不起作用?锁依然会在外部synchronized的action全部完成之后才释放了?
      

  9.   

    内部那个synchronized是没有用的,但是锁应该是在内部那个synchronized那里释放的,同时外部那个也就释放了.
      

  10.   

    你的意思就是说在进入,helper = h之前锁已经释放了?
      

  11.   

    当然不是啦,编译器会reorder代码,helper = h 会在内部那个synchronized将锁释放之前执行
      

  12.   

    对于chrisj同学认真研究问题的态度,是非常值得表扬的。我来说说的看法:
    代码本身存在的问题在于第一个if (helper == null) 不在synchronized的保护之下,从纯粹的技术上考虑,可能会存在后面的helper = h 在没有完全付值的时候(一个object的reference在jvm里为4个byte,当helper没有完成4个byte的付值的时候),此时helper既不是null,也不可用,搞不好会使系统崩溃。这种现象在32位以上的单cpu系统是不可能发生的。多cup可能出现问题不用多解释,单cup的线程切换是建立在cup指令之上的。对于一个32位以上的cpu,一个4个byte的object的reference的付值是不会被线程切换中断的。至于new Helper()可能未完成,以及class未初始化完毕(或不可预知)的说法,只能说是对java及jvm的运行机制不够了解。关于某个文章引用了晦涩难懂的词汇也并未解释清楚这段代码的问题, 写不好的文章是要误人子弟的,这就是我批评某个文章的原因。至于把里面那个synchronized单独写成个函数,我可以很负责任的说,效果是完全一样的。monitor也肯定是同一个。关于楼主的“上面这个类的锁是在何处释放的”这个问题,通俗的说,对于同一个对象的lock是在最外层的synchronized离开时被释放的。对象lock的机制:
        一个对象的lock至少存在两个参数。一个是owner,在java的synchronized里就是当前线程。另外一个参数是lock_count,初始为0。.尝试进入monitor时(进入synchronized),
    1、如果该对象没有被lock,则立即获得这个对象的lock,同时owner设置为当前线程,lock_count=1
    2、该对象有lock,owner是当前线程,lock_count加1,不等待。
    3、该对象有lock,owner不是当前线程,等待,直到该对象lock被释放,然后到第一步当离开monitor时(离开synchronized)
    lock_count减1,如果lock_count大于0,离开,不释放lock;如果lock_count等于0,释放lock
      

  13.   

    呵呵,好,大家讨论问题是最好的,我也想搞清楚这个问题,求教...几个问题:
    1.“对于一个32位以上的cpu,一个4个byte的object的reference的付值是不会被线程切换中断的。”这并不能解释我的问题,helper的reference已经不是null,可是Helper的class初始化并未完成,你只是说对jvm机制了解的不清楚,那么清楚的了解应该是怎么样的呢?我还是觉得当第二个线程进来看到helper的reference已经不是null,那么就返回了,这样拿到的helper是不可用的,有可能的情况是Helper的fields还是default value,而不是初始化后的值。2.对于lock的机制,看的不是很明白,真的要请教一下了。“lock_count减1,如果lock_count大于0,离开,不释放lock;如果lock_count等于0,释放lock”假设一个线程得到了lock,lock_count=1,这是又一个线程进来,lock_count是不是要加1变成2呢?如果是,当第一个线程要离开时,lock_count减1变成1,它就不释放lock,那么其它线程怎么进来呢如果不是,那么lock_count就还是1,又哪来的“lock_count减1,如果lock_count大于0”呢3.public Helper newHelper{
        synchronized(this);
        {
            return new Helper();
        }
    }
    这个不是相当与public synchronized Helper newHelper(){return new Helper();}吗?
    为什么跟getHelper方法里面的monitor是同一个呢?不是很明白.....4.我也不知道你看到的文章是哪一篇,不知道它是如何解释的,所以无从评论啦,或者你可以贴上来大家看看。
      

  14.   

    1、h=new Helper(),然后helper = h,这两条指令在jvm里的执行顺序是这样的:
    new Helper
    dup
    invokespecial Helper.<init>
    astore h
    ...
    aload h
    astore helper前面是new Helper,最后的astore h 和 astore helper是付值给h和helper。即使你看不懂jvm指令,也可以大概分析出执行顺序的。也就是说,付值给helper之前,是绝对保证new Helper()已经完成的,否则连单线程都无法保证正确执行。
    另外,在java里,除非你直接用ClassLoad.loadClass,其他任何方式使用到class都是初始化完成之后才可以用的。尤其是new Helper(),不能想象Helper.class未初始化完成,就能创建Helper对象。另外,即使在java里直接用Helper.class这样的代码,在jvm里也是被翻译成Class.forName("Helper")的,使用之前是完全保证类初始化完成的。
      

  15.   

    2、首先,先纠正你前面一个错误的观点:“内部那个synchronized是没有用的,但是锁应该是在内部那个synchronized那里释放的,同时外部那个也就释放了.” 在楼主的代码中对象的lock是离开外部那个synchronized时释放的。这个你可以随便问问有相关开发经验的人。“假设一个线程得到了lock,lock_count=1,这是又一个线程进来,lock_count是不是要加1变成2呢?”
        如果一个线程得到了lock,在它没有被释放之前,第二个线程只能等待。我在上面的(3)里面已经说明了这种情况。另外,这里还有个lock的等待队列的概念,而不用是lock_count来标记的。lock_count加1是指已经得到了lock的线程再一次进入这个对象的monitor的时候。关于lock_count是做什么用的,我用一个例子说明一下:
    synchronized(this)
    {
    ...
       synchronized(this)
       {
          ...
       }
    }
    这段代码在jvm里的指令如下(为了更容易理解,把jvm里的相关指令结合了一下)//lock的初始值:owner=null;lock_count=0;
    monitorenter(this) //进入前的条件 : while(owner != null && owner != currentThread) wait();
      //进入后:owner=currentThread; lock_count++;  (此时lock_count==1)
    ...
       monitorenter(this) //已经满足前面那个进入前的条件(owner==currentThread),直接进入
       lock_count++;//lock_count==2
       ...
       monitorexit(this)
       //离开monitor: lock_count--; (此时lock_count==1)
                      if(lock_count == 0)释放lock; (还不满足lock_count==0的条件,不释放lock)monitorexit(this)
      //离开monitor: lock_count--; (此时lock_count==0)
                      if(lock_count==0) 
                         owner = null;//释放lock (此时满足释放条件,释放lock,同时通知下一个等待进入这个monitor的其他线程)下面是一段用java模拟lock的例子:
    class Lock
    {
        Object owner = null;
        int lock_count = 0;
        synchronized void lock() throws InterruptedException
        {
            while(owner != null && owner != Thread.currentThread())
            {
                this.wait();
            }
            owner = Thread.currentThread();
            lock_count ++;
        }
        synchronized void unlock()
        {
            assert owner == Thread.currentThread();
            if (owner != Thread.currentThread())
                throw new Error("current thread not owner of this lock.");
            lock_count --;
            if (lock_count==0)
            {
                owner = null;
                this.notify();
            }
        }
    }
      

  16.   

    3.public Helper newHelper{
        synchronized(this);
        {
            return new Helper();
        }
    }
    这个不是相当与public synchronized Helper newHelper(){return new Helper();}吗?
    为什么跟getHelper方法里面的monitor是同一个呢?不是很明白.....monitor是针对某个对象的。如果存在object1==object2,那么object1和object2的monitor就是同一个。
    public synchronized ...
    {
    }

    public ...
    {
       synchronized(this)
       {
         ...
        }
    }
    是等价的,这个是常识。不过直接写在方法定义上的那个在class里看不到monitor相关的指令,它由jvm自动完成,并不影响两个代码相同的结果。4、关于那篇文章,还是没必要看,不懂的搞不好会更晕。线程相关的学习资料很多,同时也要自己多写一些代码联系。例如:让面那个lock什么时候的问题,写一小段程序都可以测试出来:
    public class ThreadTest implements Runnable 
    {
        void log(String msg)
        {
            System.out.println("[" + new java.util.Date() 
                               + "]\t[" + Thread.currentThread().getName()
                               + "]\t" + msg);
        }
        void doSomething()
        {
            try
            {
                Thread.sleep(2000);//足够的达到效果的时间
            }
            catch(Throwable _)
            {
                
            }
        }
        public void run()
        {
            log("尝试第一次进入Monitor.");
            synchronized(this)
            {
                log("第一次成功进入Monitor");
                this.doSomething(); //此处可以去掉,可证明可以马上第二次进入Monitor.
                log("尝试第二次进入Monitor.");
                synchronized(this)
                {
                    log("第二次成功进入Monitor");
                    this.doSomething();
                    log("将要离开第二次进入的Monitor");
                }
                log("已经离开第二次进入的Monitor");
                this.doSomething();//此处要有足够的时间,可以看到lock并未释放
                log("将要离开第一次进入的Monitor");
            }
            log("已经离开第一次进入的Monitor");
        }
        public static void main(String[] args)
        {
            ThreadTest test = new ThreadTest();
            new Thread(test,"线程#1").start();
            new Thread(test,"线程#2").start();
        }
    }
    运行结果:
    [Wed Sep 28 00:13:47 CST 2005] [线程#1] 尝试第一次进入Monitor.
    [Wed Sep 28 00:13:47 CST 2005] [线程#1] 第一次成功进入Monitor
    [Wed Sep 28 00:13:47 CST 2005] [线程#2] 尝试第一次进入Monitor.
    [Wed Sep 28 00:13:49 CST 2005] [线程#1] 尝试第二次进入Monitor.
    [Wed Sep 28 00:13:49 CST 2005] [线程#1] 第二次成功进入Monitor
    [Wed Sep 28 00:13:51 CST 2005] [线程#1] 将要离开第二次进入的Monitor
    [Wed Sep 28 00:13:51 CST 2005] [线程#1] 已经离开第二次进入的Monitor
    [Wed Sep 28 00:13:53 CST 2005] [线程#1] 将要离开第一次进入的Monitor
    [Wed Sep 28 00:13:53 CST 2005] [线程#2] 第一次成功进入Monitor
    [Wed Sep 28 00:13:53 CST 2005] [线程#1] 已经离开第一次进入的Monitor //这里跟上一条信息几乎是同时发生的,顺序从原理上来讲不可预知。
    [Wed Sep 28 00:13:55 CST 2005] [线程#2] 尝试第二次进入Monitor.
    [Wed Sep 28 00:13:55 CST 2005] [线程#2] 第二次成功进入Monitor
    [Wed Sep 28 00:13:57 CST 2005] [线程#2] 将要离开第二次进入的Monitor
    [Wed Sep 28 00:13:57 CST 2005] [线程#2] 已经离开第二次进入的Monitor
    [Wed Sep 28 00:13:59 CST 2005] [线程#2] 将要离开第一次进入的Monitor
    [Wed Sep 28 00:13:59 CST 2005] [线程#2] 已经离开第一次进入的Monitor
      

  17.   

    首先,真的感谢taolei这么认真的回答问题,btw,向楼上凌晨2点还回贴的兄弟致敬...
    其次,我得承认我犯的一个低级错误,就是两个monitor是不是同一个的问题,想想就知道肯定是啦,hehe.
    1.我想这是我和taolei之间最大的分歧吧.我还是认为,在java中,对helper的reference赋一个还没有初始化的Helper是可能的,其实Double Check在java中能不能用这个问题已经被讨论了很久了,有人为了验证就写了一个Class,然后用Symantec JIT去监视它的执行,结果证明了这一点,也就是说对一个reference赋了一个还没初始化的Class object.可以去google一下DoubleCheckTest这个类名,应该可以找到源代码.我以前看过的一些包括Sun公司的资料中的观点都是这样.这可以说是java memory model的一个缺陷,但是我不肯定的一点是java是否做过memory model的改进或者什么时候进行了改进,因为我近期没有去查过这个问题.2.对于同一个线程连续两次去拿一个lock的机制,首先我完全同意taolei的逻辑是完全行的通的,但是就我所知,java却不是这样实现的.首先,大家一眼就看得出,内部的synchronized其实没有什么意义,jvm在看到同一个线程连续第二次获取同一个lock的时候,它的处理其实相当于"当它不存在",jvm会reorder代码的,在执行的时候变成:synchronized(this) {
        h = helper;
        if (h == null) 
            synchronized (this) {
                h = new Helper();
                helper = h;
            } 
    }内部的synchronized虽然被保留了下来,其实效果上等于根本不存在.对于用lock_count来控制的逻辑,我完全同意行得通,但两种方式对jvm来说实现起来哪一种更简单方便呢?尤其是内部syn根本就没什么意义?我觉得如果是我来做jvm,我不会选taolei的方案,hehe.其实,有人在代码层次上实现lock的机制,是开源的代码,可能叫Mutex什么的,不是很记得了,里面用的就是taolei所讲的lock_count方法. 用这个开源代码的api在代码级控制可以做到象用java的api一样的效果.可是,据我所知(hehe,我必须得强调这一点),jvm不是靠lock_count.那么,如果按照我的解释,taolei写的那个测试lock什么时候被释放的例子已经没什么意义了,因为在代码在执行时被reorder了之后,在内部syn结束和外部syn结束之间已经没有动作了.
      

  18.   

    中间的synchronized会被reorder省略掉?!,那是不可能的。
    Object x1,x2
    synchronized(x1)
    {
    synchronized(x2)
    {
    }
    }
    x1,x2两个对象是否为同一个,JVM是不可能预先知道的,更不可能会把里面的synchronized reorder省略掉。"this"在jvm运行期也是一个local变量,编号为0,对synchronized而言,跟x1,x2是没有什么区别的。某些观点太一厢情愿了。都是一些自己都没理解的名词,想当然的认为会怎样。不打算再讨论下去了。
      

  19.   

    受教了,谢谢taolei和chrisj的讨论,基本理解了。