Java concurrency scenario — do I need synchronization or not?

后端 未结 10 1563
陌清茗
陌清茗 2021-02-01 07:19

Here\'s the deal. I have a hash map containing data I call \"program codes\", it lives in an object, like so:

Class Metadata
{
    private HashMap validProgramC         


        
10条回答
  •  小鲜肉
    小鲜肉 (楼主)
    2021-02-01 08:07

    Use Volatile

    Is this a case where one thread cares what another is doing? Then the JMM FAQ has the answer:

    Most of the time, one thread doesn't care what the other is doing. But when it does, that's what synchronization is for.

    In response to those who say that the OP's code is safe as-is, consider this: There is nothing in Java's memory model that guarantees that this field will be flushed to main memory when a new thread is started. Furthermore, a JVM is free to reorder operations as long as the changes aren't detectable within the thread.

    Theoretically speaking, the reader threads are not guaranteed to see the "write" to validProgramCodes. In practice, they eventually will, but you can't be sure when.

    I recommend declaring the validProgramCodes member as "volatile". The speed difference will be negligible, and it will guarantee the safety of your code now and in future, whatever JVM optimizations might be introduced.

    Here's a concrete recommendation:

    import java.util.Collections;
    
    class Metadata {
    
        private volatile Map validProgramCodes = Collections.emptyMap();
    
        public Map getValidProgramCodes() { 
          return validProgramCodes; 
        }
    
        public void setValidProgramCodes(Map h) { 
          if (h == null)
            throw new NullPointerException("validProgramCodes == null");
          validProgramCodes = Collections.unmodifiableMap(new HashMap(h));
        }
    
    }
    

    Immutability

    In addition to wrapping it with unmodifiableMap, I'm copying the map (new HashMap(h)). This makes a snapshot that won't change even if the caller of setter continues to update the map "h". For example, they might clear the map and add fresh entries.

    Depend on Interfaces

    On a stylistic note, it's often better to declare APIs with abstract types like List and Map, rather than a concrete types like ArrayList and HashMap. This gives flexibility in the future if concrete types need to change (as I did here).

    Caching

    The result of assigning "h" to "validProgramCodes" may simply be a write to the processor's cache. Even when a new thread starts, "h" will not be visible to a new thread unless it has been flushed to shared memory. A good runtime will avoid flushing unless it's necessary, and using volatile is one way to indicate that it's necessary.

    Reordering

    Assume the following code:

    HashMap codes = new HashMap();
    codes.putAll(source);
    meta.setValidProgramCodes(codes);
    

    If setValidCodes is simply the OP's validProgramCodes = h;, the compiler is free to reorder the code something like this:

     1: meta.validProgramCodes = codes = new HashMap();
     2: codes.putAll(source);
    

    Suppose after execution of writer line 1, a reader thread starts running this code:

     1: Map codes = meta.getValidProgramCodes();
     2: Iterator i = codes.entrySet().iterator();
     3: while (i.hasNext()) {
     4:   Map.Entry e = (Map.Entry) i.next();
     5:   // Do something with e.
     6: }
    

    Now suppose that the writer thread calls "putAll" on the map between the reader's line 2 and line 3. The map underlying the Iterator has experienced a concurrent modification, and throws a runtime exception—a devilishly intermittent, seemingly inexplicable runtime exception that was never produced during testing.

    Concurrent Programming

    Any time you have one thread that cares what another thread is doing, you must have some sort of memory barrier to ensure that actions of one thread are visible to the other. If an event in one thread must happen before an event in another thread, you must indicate that explicitly. There are no guarantees otherwise. In practice, this means volatile or synchronized.

    Don't skimp. It doesn't matter how fast an incorrect program fails to do its job. The examples shown here are simple and contrived, but rest assured, they illustrate real-world concurrency bugs that are incredibly difficult to identify and resolve due to their unpredictability and platform-sensitivity.

    Additional Resources

    • The Java Language Specification - 17 Threads and Locks sections: §17.3 and §17.4
    • The JMM FAQ
    • Doug Lea's concurrency books

提交回复
热议问题