Read field stale value after object construction

前端 未结 4 1958
甜味超标
甜味超标 2021-02-05 07:58

I\'m reading a book "Java concurrency in practice" by Brian Goetz. Paragraphs 3.5 and 3.5.1 contains statements that I can not understand.

Consider the followin

相关标签:
4条回答
  • 2021-02-05 08:22

    Sleeping 3 seconds before assigning the field in the constructor does not matter because for value != value to be true, the first read of value must produce a different result than the second one, which happens immediately after.

    The Java Memory Model does not guarantee that values assigned to fields in constructors are visible to other threads after the constructor finishes. To have this guarantee, the field must be final.

    Here's a program that produces the bug on x86. It must be run with the VM option: -XX:CompileCommand=dontinline,com/mypackage/Holder.getValue

    package com.mypackage;
    
    public class Test {
        public static void main(String[] args) {
            new Worker().start();
            int i = 1;
            while (true) {
                new Holder(i++);
            }
        }
    }
    
    class Holder {
        private int value;
    
        Holder(int value) {
            Worker.holder = this;
            this.value = value;
        }
    
        void assertSanity() {
            if (getValue() != getValue()) throw new AssertionError();
        }
    
        private int getValue() { return value; }
    }
    
    class Worker extends Thread {
        static Holder holder = new Holder(0);
    
        @Override
        public void run() {
            while (true) {
                holder.assertSanity();
            }
        }
    }
    

    By disallowing Holder#getValue() to be inlined, we prevent the two subsequent reads of value to be collapsed into a single one.

    This optimization prevents the code in the book from producing the bug. However, the book author is still correct, since this optimization is not mandatory, so from the Java Memory Model perspective, the code is incorrect.

    The assertSanity() method is equal to:

    int snapshot1 = getValue();
                                // <--- window of vulnerability, where the observed value can change
                                //      if you chose to sleep 3 seconds, you would want to do it here
                                //      takes very little time, less than 1 nanosecond
    int snapshot2 = getValue();
    if (snapshot1 != snapshot2) throw new AssertionError();
    

    So the first read of value could produce the default value of int which is 0 (called the stale value and assigned in the Object() constructor), and the second read could produce the value assigned in the Holder(int) constructor. This would happen if for example the value assigned in the constructor were propagated to the thread calling assertSanity() in the exact moment between the two loads of value (window of vulnerability).

    The same would happen if we delayed the second read in some other way, like:

    int snapshot1 = this.value;
    Thread.interrupted();
    int snapshot2 = this.value;
    if (snapshot1 != snapshot2) throw new AssertionError();
    
    0 讨论(0)
  • 2021-02-05 08:27

    You are probably trying to simulate a concurrency scenario, which I believe is very hard to simulate using a couple of threads.

    The following test-case which you have written is not correct at all is more likely to throw a NullPointerException.

    public class Tests {
    
      private HolderContainer hc = new HolderContainer();
    
      class Initialization implements Runnable {
        public void run() {
          hc.init();
        }
      }
    
      class Checking implements Runnable {
        public void run() {
          hc.holder.assertValue();
        }
      }
    
      public void run() {
        new Thread(new Initialization()).start();  
        new Thread(new Checking()).start(); 
      }
    }
    

    What if your Checking Thread executes before Initialization one?? Also putting a sleep there simply means that executing thread will sleep and does tell you about the internal atomic operations being performed by then.

    0 讨论(0)
  • 2021-02-05 08:32

    Did not reproduce it with your code. Here is an example to emulate un-safe publication. The strategy is let one thread publication Holder and let another check its value.

    class Holder {
        private volatile int value;
    
        public Holder(int value, HolderContainer container) {
            container.holder = this;  // publication this object when it is not initilized properly
            try {
                Thread.sleep(10);  
            } catch (Exception e) {
    
            }
            this.value = value; // set value
        }
    
        public int getValue() {
            return value;
        }
    }
    
    class HolderContainer {
    
        public Holder holder;
    
        public Holder getHolder() { 
            if (holder == null) { 
                holder = new Holder(42, this);
            }
            return holder;
        }
    }
    
    
    public class Tests {
    
        public static void main(String[] args) {
            for (int loop = 0; loop < 1000; loop++) {
                HolderContainer holderContainer = new HolderContainer();
                new Thread(() -> holderContainer.getHolder()).start();
                new Thread(() -> {
                    Holder holder = holderContainer.getHolder();
                    int value1 = holder.getValue();  // might get default value
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
    
                    }
                    int value2 = holder.getValue(); // might get custom value
                    if (value1 != value2) {
                        System.out.println(value1 + "--->" + value2);
                    }
                }).start();
            }
        }
    
    }
    
    0 讨论(0)
  • 2021-02-05 08:35

    I tried to test the problem with the following code.

    Test:

    public class Test {
        public static boolean flag =true;
        public static HolderContainer hc=new HolderContainer();
    
        public static void main (String args[]){    
            new Thread(new Initialization()).start();
            new Thread(new Checking()).start();
        }
    }
    
    class Initialization implements Runnable {
        public void run() {
            while (Test.flag){
                Test.hc=new HolderContainer();
                Test.hc.init();
                }
        }
    }
    
    class Checking implements Runnable {
        public void run() {
            try{
                Test.hc.holder.assertValue();
            }
            catch (NullPointerException e) {
            }    
        }
    }
    

    Holder:

    public class Holder {
        private int value;
            public Holder(int value) { 
            this.value = value;
        }
    
        public void assertValue() {
            if (value != value) {
                System.out.println("Magic");
                Test.flag=false;
            }
        }
    }
    
    class HolderContainer {
        public Holder holder;
        public void init() {
            holder = new Holder(42);  
        }
    }
    

    I never got the program to evaluate value!=value to true. I don't think this proves anything and didn't run it for more than a couple minutes, but I hope this will be a better starting point for a well designed test or at least help to figure out some possible flaws in the tests.

    I tried to insert a sleep between Test.hc=new HolderContainer(); and Test.hc.init();, between public Holder holder; and public void init() { and after public void init() {.

    I am also concerned that checking if a value is null or catching the NullPoiterException may affect the timing too much.

    Please note that the currently accepted answer to Improper publication of Java Object Reference says this problem is probably impossible under an x86 architecture. It may also be JVM dependent.

    0 讨论(0)
提交回复
热议问题