【问题标题】:synchronise on non-final object [duplicate]在非最终对象上同步[重复]
【发布时间】:2013-04-20 21:48:27
【问题描述】:
private volatile Object obj = new MyObject();
void foo() 
{
  synchronized(obj)
  {
    obj.doWork();
  }
}
void bar()
{
  synchronized(obj)
  {
    obj.doWork();
    obj = new MyObject(); // <<<< notice this line (call it line-x)
  }
}

假设在某个时间点,一个线程t_bar正在执行bar(),另一个t_foo正在执行foo,而那个t_bar刚刚获取到obj,所以t_foo实际上是在等待。

bar 中的同步块执行后,foo 将执行其同步块,对吗?它会看到obj 的什么值?旧的?还是bar中的新设置?

(我希望看到新值,这就是这样编码的重点,但我想知道这是否是一个“安全”的赌注)

【问题讨论】:

  • @javapirate:我觉得您编辑我的帖子只是因为您更喜欢自己的 K&R 格式样式,这非常粗鲁?很抱歉,我必须重新格式化它。
  • 对不起。我想摆脱很多空白。来吧!
  • 一个可能有助于理解的区别是 objects 或非最终或非最终的,持有对它们的引用的变量是。锁在对象上,而不是在变量上。
  • @MiserableVariable 我明白,但感谢您的澄清!

标签: java synchronize


【解决方案1】:

这是不安全且损坏的。更改您锁定的对象不起作用。

当一个线程试图进入一个同步块时,它首先必须计算括号中的表达式,以确定它需要什么锁。如果在那之后锁发生了变化,线程没有任何办法知道,它最终会获取旧锁并进入同步块。此时,它看到对象并对其进行评估,获取新的引用,并使用旧的(现在不相关的)锁调用它的方法,而不持有新的锁,即使其他线程可能持有新的锁并且可以同时在同一个对象上执行该方法。

【讨论】:

    【解决方案2】:

    它会正常运行,就好像对象引用没有在内部更改一样。原因是对象锁定测试只会进行一次。因此,即使对象在内部发生变化,线程也会继续等待,并且行为将与对象相同 [未更改] 相同。

    我尝试了另一件事。我在新对象创建后放置了一个睡眠语句,然后启动下一个线程,并且正如预期的那样,两个线程同时开始工作。 请参阅下面的代码。

    public class ChangeLockObjectState {
    
        private volatile Object obj = new Object();
    
        void foo() {
            synchronized (obj) {
                try {
                    System.out.println("inside foo");
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    
        void bar() {
            synchronized (obj) {
                try {
                    System.out.println("inside bar");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
    
                obj = new Object(); // <<<< notice this line (call it line-x)
    
                System.out.println("going out of  bar");
    
                try {
    
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
    
                System.out.println("wait over");
    
            }
        }
    
        /**
         * @param args
         * @throws InterruptedException
         */
        public static void main(String[] args) throws InterruptedException {
            final ChangeLockObjectState test = new ChangeLockObjectState();
    
            new Thread(new Runnable() {
    
                @Override
                public void run() {
                    test.bar();
    
                }
            }).start();
    
            Thread.sleep(6000);
    
            new Thread(new Runnable() {
    
                @Override
                public void run() {
                    test.foo();
    
                }
            }).start();
    
        }
    
    }
    

    【讨论】:

      【解决方案3】:

      将读取obj 的新值。

      来自Happens before 上的标准部分:

      对 volatile 字段(第 8.3.1.4 节)的写入发生在对该字段的每次后续读取之前。

      从共享变量的定义:

      所有实例字段、静态字段和数组元素都存储在堆内存中。在本章中,我们使用术语变量来指代字段和数组元素。 局部变量(第 14.4 节)、形式方法参数(第 8.4.1 节)和异常处理程序参数(第 14.20 节)永远不会在线程之间共享,并且不受内存模型的影响。

      在同步块内读取obj 与表达式obj 的初始评估是分开的,以确定要锁定哪个对象的内置监视器。 obj 的重新分配将发生在第一次读取之前,而不是第二次。由于objvolatile 字段,因此第二次读取必须看到obj 的更新值。

      【讨论】:

      • 它与这个问题有什么联系?
      • 哎呀,你是对的。回答了错误的问题:-(
      • 改为回答正确的问题。
      【解决方案4】:

      显示新值。即使没有制作objvolatile,它也可以工作。这是因为同步仍然保留在旧对象上,并在等待线程 (t_foo) 进入后提供对新值的可见性。这是测试:

      public class Main3 {
          private MyObject obj = new MyObject(1);
      
          void foo()
          {
              synchronized(obj)
              {
                  System.out.println(obj.number);
                  obj.doWork();
              }
          }
      
          void bar()
          {
              synchronized(obj)
              {
                  System.out.println(obj.number);
      
                  obj.doWork();
      
                  //force the foo thread to wait at the synchronization point
                  for(int i = 0; i < 1000000000l; i++);
      
                  obj = new MyObject(2); // <<<< notice this line (call it line-x)
              }
          }
      
          public static void main(String[] args) throws InterruptedException {
              final Main3 m3 = new Main3();
      
              Thread t1 = new Thread( new Runnable() {
                  @Override
                  public void run() {
                      m3.bar();
                  }
              });
      
              Thread t2 = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      m3.foo();
                  }
              });
      
              t1.start();
              t2.start();
          }
      }
      
      class MyObject {
          int number;
      
          public MyObject(int number) {
              this.number = number;
          }
      
          public void doWork() {
          }
      }
      

      【讨论】:

        【解决方案5】:

        在您描述的确切情况下,是的,在 foo 的同步块中读取 obj 将看到前一个 bar 的同步块设置的新值。

        有趣的是,它并不总是在那种确切的情况下发生。该程序不是线程安全的,例如,如果在bar() 退出后立即调用另一个bar(),而 foo 线程正在锁定旧对象。 bar线程锁在新对象上,所以两个线程同时执行,都在同一个新obj上执行obj.doWork()

        我们可能可以部分修复它

        // suppose this line happens-before foo()/bar() calls
        MyObject obj = new MyObject();
        
        void foo()
            while(true)
                MyObject tmp1 = obj;
                synchronized(tmp1)
                    MyObject tmp2 = obj;
                    if(tmp2==tmp1)
                        tmp2.doWork();
                        return;
                    // else retry
        

        这至少保证当前不会在同一个 obj 上调用 obj.doWork(),因为 obj.doWork() 只能发生在锁定完全相同的 obj 的同步块中

        【讨论】:

        • foo 怎么还在锁定old 对象?在bar 退出后,obj 到处都会有新的价值,是吗?
        • @One 不,foo 仍将锁定旧对象。请参阅docs.oracle.com/javase/specs/jls/se7/html/jls-14.html#jls-14.19,或查看我的问题 2 编辑前的答案(我在其中回答了 那个 问题。
        • 好的,我理解它持有旧对象的锁。但看起来同步块 inside 的任何引用都将引用新值 ob obj...?
        • foo线程锁定在旧对象上,然后在块内部,它看到新对象,因此在旧对象的锁定下对新对象进行工作。
        猜你喜欢
        • 2023-04-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-12-27
        • 2015-07-13
        • 2016-05-08
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多