On May 11, 2012, at 8:00 PM, Adam Heath wrote:

> On 05/11/2012 12:30 PM, Jacopo Cappellato wrote:
>> 
>> On May 11, 2012, at 6:34 PM, Adam Heath wrote:
>> 
>>> There is no difference.  The act of map.get() can be doing a *lot* of
>>> separate, individual steps.  Without any kind of
>>> happens-before/happens-after on map.get(), the protected put/get in
>>> the lock could interleave with the unprotected get.
>> 
>> Ok, if you don't want to admit that there is no DCL in my code I will give 
>> up; what you describe next is a clearly totally different topic.
> 
> I won't admit that, because there IS a DCL in the code you added in
> the original commit.
> 
> if (!initialized) {
>  lock {
>    if (!initialized) {
>      initialize();
>    }
>  }
> }
> 

This is not what I implemented; I already explained it in my other emails, 
please read them carefully.I have to go now but if you can't find the answers 
from my others email let me know and I will add details during the week end 
(now I have to go).

> That is DCL to.  It doesn't matter if initialized is a simple read of
> a variable, a null check, or something more complex(map.get).  They
> are all the same *pattern*.
> 
> What is at issue, is that the code running before the lock can be
> interleaved with the code run inside the lock.  This is because the
> code outside the lock has no barrier, no guard, no kind of
> happens-relationship at all with the code inside the lock.  All the
> lock helps protect is 2 threads trying to run the internal block.  But
> that external block can run at any time.

There is no need to explain to me why DCL is bad.

> 
>> But even if a totally different topic I think it is an interesting one 
>> because the UtilCache get/put/putIfAbsent methods are used a lot and if they 
>> are not thread safe this would be a problem. A problem for my code, your 
>> code and a lot of code in OFBiz.
> 
> The UtilCache objects *are* thread-safe.  You will not get any
> internal corruption inside UtilCache.  That is all that can be guaranteed.
> 
> What isn't thread safe, is that calling code may end up having
> multiple instances for the same key, unless calling code is written
> correctly.

And so if these methods are thread safe all your comments below don't apply to 
the code I wrote:

>> Saying again.  2 threads could be running.  One wants "foo", the other
>> wants "bar".  The first sees no value for "foo", starts generating the
>> result, then calls map.put("foo", result).  When map.put is running,
>> it's doing a lot of separate, individual steps.
>> 
>> Now, the second thread comes around, and calls map.get("bar").  The
>> first thread is still running inside the put.  The second thread is
>> not in the synchronized block, so it went straight into map.get.  And
>> now you have 2 threads both running code inside that map that has no
>> happens-before/happens-after guarantees, so now things blow up.


> 
> The changes I did to remove synchronized from most places in UtilCache
> were correct, it reduced contention.  Each method on UtilCache still
> has correct happens-before/happens-since, both before and after
> removing synchronized.
> 

And in fact I greatly appreciate the work you did at that time.

> Any such issues in calling code were always there previously; it's
> just that because of synchronized, it slowed down threads enough so
> that you wouldn't see bad code as often.  Remove the contention,
> things happen quicker, and so do any bugs.
> 
> Why do you think java 1.1 introduced HashMap, to replace Hashtable?
> Hashtable used to have synchronized on get/put, but in reality, that
> did *nothing* to protect calling code that had *separate* calls to
> get/put/containsKey/whatever.  It is the *external* code that needed
> the lock, not the Hashtable.
> 
> Also, an unprotected map.entrySet().iterator() can have corruption if
> other threads are mutating the structure of the map.  In those case,
> external synchronization needs to happen between the iteration, *and*
> the get/put/remove.  If you read the ConcurrentMap(and friends) code,
> you'll find references to the iterators failing
> fast(ConcurrentModificationException), or not failing at all(you'll
> get a read-only snapshot of something that may not reflect the current
> values of the Collection).

Yeah, I know all of these things: "Java Concurrency In Practice" is actually 
quite enlightening on these topics.

Regards,

Jacopo

> 
>> I didn't look too closely at the implementation of the two methods but it 
>> seems that they are actually handling the shared status in a thread safe way 
>> (even if they are not atomic), so I do not see a risk for reading objects in 
>> a stale/invalid status; but you actually did, if I am not wrong, the 
>> implementation of these method and you may know better than I.
> 
> I have seen a map.get/null-check/put DCL as given in your code cause
> threads to enter into never-ending loops, as they call .toString(),
> which calls .iterator(), which, because of unprotected manipulation,
> the hash-bucket links internal to the map become circular.
> 
> The first time(it's happened multiple times), I had no clue.  But
> then, I added a -XX parameter to allow me to attach to the java
> process.  The next time, I dumped the entire memory(that was huge, as
> these bugs happen more often during OOM).  I then used IBM's
> HeapAnaylzer(nice little swing app to memory dumps, better than jhat)
> to see that the hashbucket links were circular.
> 
>> By the way, all the issues that you are mentioning are actually all present 
>> in the fix that you suggested at the beginning of this thread:
>> 
>> Class scriptClass = parsedScripts.get(script);
>> if (scriptClass == null) {
>> scriptClass = genClass();
>> parsedScripts.putIfAbsent(script, scriptClass);
>> scriptClass = parsedScripts.get(script);
>> }
> 
> parsedScripts is a UtilCache; the methods on that object have
> happens-before/happens-after guarantees, due to internally using
> ConcurrentMap.  Deep inside ConcurrentMap, you'll find variables
> marked volatile, and/or work-stealing/work-completing patterns, that
> also again have happens-before/happens-after.
> 
> The only issue in the above code, as I have already mentioned, is that
> some other background thread may call remove(script) inbetween
> putIfAbsent() and the following get().  And the solution for that is
> to change the if to a while.
> 
>> while my initial code was safer (because it was wrapping get and put calls 
>> into synchronized blocks). But if the UtilCache method put/get/putIfAbsent 
>> are not thread safe, now it makes me nervous to call them within 
>> synchronized blocks because in them there are also synchronized blocks (risk 
>> for deadlocks). The best way to fix this would be to make the methods thread 
>> safe (if they are not already as it seems they are).
> 
> Nope, your code still had the same removal problem.
> 
>>> Yet another thread might explicitly *remove* the key between the
>>> putIfAsent and following get; so the above really needs to be done in
>>> a loop.  With UtilCache, an item may be removed *at any time* when
>>> soft-references are used, or when a TTL expires.  Even in such a short
>>> timeframe as the 2 lines above.  Just because those 2 lines are right
>>> next to each other, doesn't mean that the process execution context
>>> will stay running this particular thread.
>> 
>> This is a good point and probably the only real issue of your initial code 
>> (and in the latest version of my code, the one without synchronized blocks); 
>> I would suggest the following fix:
>> 
>> Class scriptClass = parsedScripts.get(script);
>> if (scriptClass == null) {
>> scriptClass = genClass();
>> Class cachedScriptClass = null;
>> do {
>>   parsedScripts.putIfAbsent(script, scriptClass);
>>   cachedScriptClass = parsedScripts.get(script);
>> } while(cachedScriptClass != null);
>> scriptClass = cachedScriptClass;
>> }
>> 
>> But of course here I am assuming that UtilCache put/get/putIfAbsent are 
>> thread safe.
> 
> They are, just without doing synchronized.
> 
>> What do you think?
>> 
>> Jacopo
>> 
> 

Reply via email to