Java concurrency scenario — do I need synchronization or not?

后端 未结 10 1560
陌清茗
陌清茗 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 07:51

    While this is not the best solution for this particular problem (erickson's idea of a new unmodifiableMap is), I'd like to take a moment to mention the java.util.concurrent.ConcurrentHashMap class introduced in Java 5, a version of HashMap specifically built with concurrency in mind. This construct does not block on reads.

    0 讨论(0)
  • 2021-02-01 07:52

    As others have already noted, this is not safe and you shouldn't do this. You need either volatile or synchronized here to force other threads to see the change.

    What hasn't been mentioned is that synchronized and especially volatile are probably a lot faster than you think. If it's actually a performance bottleneck in your app, then I'll eat this web page.

    Another option (probably slower than volatile, but YMMV) is to use a ReentrantReadWriteLock to protect access so that multiple concurrent readers can read it. And if that's still a performance bottleneck, I'll eat this whole web site.

      public class Metadata
      {
        private HashMap validProgramCodes;
        private ReadWriteLock lock = new ReentrantReadWriteLock();
    
        public HashMap getValidProgramCodes() { 
          lock.readLock().lock();
          try {
            return validProgramCodes; 
          } finally {
            lock.readLock().unlock();
          }
        }
    
        public void setValidProgramCodes(HashMap h) { 
          lock.writeLock().lock();
          try {
            validProgramCodes = h; 
          } finally {
            lock.writeLock().unlock();
          }
        }
      }
    
    0 讨论(0)
  • 2021-02-01 07:53

    Check this post about concurrency basics. It should be able to answer your question satisfactorily.

    http://walivi.wordpress.com/2013/08/24/concurrency-in-java-a-beginners-introduction/

    0 讨论(0)
  • 2021-02-01 08:02

    No, by the Java Memory Model (JMM), this is not thread-safe.

    There is no happens-before relation between writing and reading the HashMap implementation objects. So, although the writer thread appears to write out the object first and then the reference, a reader thread may not see the same order.

    As also mentioned there is no guarantee that the reaer thread will ever see the new value. In practice with current compilers on existing hardware the value should get updated, unless the loop body is sufficienly small that it can be sufficiently inlined.

    So, making the reference volatile is adequate under the new JMM. It is unlikely to make a substantial difference to system performance.

    The moral of this story: Threading is difficult. Don't try to be clever, because sometimes (may be not on your test system) you wont be clever enough.

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

    If I read the JLS correctly (no guarantees there!), accesses to references are always atomic, period. See Section 17.7 Non-atomic Treatment of double and long

    So, if the access to a reference is always atomic and it doesn't matter what instance of the returned Hashmap the threads see, you should be OK. You won't see partial writes to the reference, ever.


    Edit: After review of the discussion in the comments below and other answers, here are references/quotes from

    Doug Lea's book (Concurrent Programming in Java, 2nd Ed), p 94, section 2.2.7.2 Visibility, item #3: "

    The first time a thread access a field of an object, it sees either the initial value of the field or the value since written by some other thread."

    On p. 94, Lea goes on to describe risks associated with this approach:

    The memory model guarantees that, given the eventual occurrence of the above operations, a particular update to a particular field made by one thread will eventually be visible to another. But eventually can be an arbitrarily long time.

    So when it absolutely, positively, must be visible to any calling thread, volatile or some other synchronization barrier is required, especially in long running threads or threads that access the value in a loop (as Lea says).

    However, in the case where there is a short lived thread, as implied by the question, with new threads for new readers and it does not impact the application to read stale data, synchronization is not required.


    @erickson's answer is the safest in this situation, guaranteeing that other threads will see the changes to the HashMap reference as they occur. I'd suggest following that advice simply to avoid the confusion over the requirements and implementation that resulted in the "down votes" on this answer and the discussion below.

    I'm not deleting the answer in the hope that it will be useful. I'm not looking for the "Peer Pressure" badge... ;-)

    0 讨论(0)
  • 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
    0 讨论(0)
提交回复
热议问题