I have a webapp that I am in the middle of testing load and performance, especially on a function where we expect several hundred users to access the same page and be updated every 10 seconds on that page. One area of ​​improvement we discovered with this feature was caching responses from the web service for a period of time, as the data did not change.
After implementing this basic caching in some subsequent tests, it turned out that I did not consider the possibility of simultaneously accessing the cache at the same time. I found that for ~ 100 ms, about 50 threads tried to retrieve the object from the cache, finding that it expired, hitting the web service to retrieve the data, and then returning the object to the cache.
The original code looked something like this:
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { final String key = "Data-" + email; SomeData[] data = (SomeData[]) StaticCache.get(key); if (data == null) { data = service.getSomeDataForEmail(email); StaticCache.set(key, data, CACHE_TIME); } else { logger.debug("getSomeDataForEmail: using cached object"); } return data; }
So, to make sure that only one thread called the web service when the object in key expired, it seemed to me that I needed to synchronize the get / set Cache operation, and it seemed that using the cache key would be a good candidate for the object to synchronize (thus , a call to this method for e-mail b@b.com will not be blocked by calls to methods at a@a.com).
I updated the method so that it looks like this:
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { SomeData[] data = null; final String key = "Data-" + email; synchronized(key) { data =(SomeData[]) StaticCache.get(key); if (data == null) { data = service.getSomeDataForEmail(email); StaticCache.set(key, data, CACHE_TIME); } else { logger.debug("getSomeDataForEmail: using cached object"); } } return data; }
I also added log lines for things like “in front of the synchronization block”, “inside the synchronization block”, “about exiting the synchronization block” and “after the synchronization block”, so I could determine if I got / get synchronized efficiently operation.
However, it does not seem to have worked. My test logs have an output, for example:
(log output is 'thread_name' 'log name' 'message')
http-80-Processor253 jsp.view-page - getSomeDataForEmail: about to enter a synchronization block
http-80-Processor253 jsp.view-page - getSomeDataForEmail: internal synchronization block
http-80-Processor253 cache.StaticCache - get: object in key [SomeData-test@test.com] has expired
http-80-Processor253 cache.StaticCache - get: key [SomeData-test@test.com] return value [null]
http-80-Processor263 jsp.view-page - getSomeDataForEmail: about to enter a synchronization block
http-80-Processor263 jsp.view-page - getSomeDataForEmail: internal synchronization block
http-80-Processor263 cache.StaticCache - get: object with key [SomeData-test@test.com] has expired
http-80-Processor263 cache.StaticCache - get: key [SomeData-test@test.com] return value [null]
http-80-Processor131 jsp.view-page - getSomeDataForEmail: about to enter a synchronization block
http-80-Processor131 jsp.view-page - getSomeDataForEmail: internal synchronization block
http-80-Processor131 cache.StaticCache - get: object in key [SomeData-test@test.com] has expired
http-80-Processor131 cache.StaticCache - get: key [SomeData-test@test.com] returning value [null]
http-80-Processor104 jsp.view-page - getSomeDataForEmail: internal synchronization block
http-80-Processor104 cache.StaticCache - get: object with key [SomeData-test@test.com] has expired
http-80-Processor104 cache.StaticCache - get: key [SomeData-test@test.com] return value [null]
http-80-Processor252 jsp.view-page - getSomeDataForEmail: about to enter a synchronization block
http-80-Processor283 jsp.view-page - getSomeDataForEmail: about to enter a synchronization block
http-80-Processor2 jsp.view-page - getSomeDataForEmail: about to enter a synchronization block
http-80-Processor2 jsp.view-page - getSomeDataForEmail: internal synchronization block
I wanted to see only one thread at a time entering / leaving the synchronization block around get / set operations.
Is there a problem in synchronizing String objects? I thought the cache key would be a good choice, since it is unique to the operation, and although the final String key declared in this method, I thought that each thread would receive a link to the same object and therefore there would be synchronization on this separate object.
What am I doing wrong here?
Update : after looking in the logs, it looks like methods with the same synchronization logic, where the key is always the same, for example
final String key = "blah"; ... synchronized(key) { ...
the same concurrency problems are not displayed - only one thread at a time enters the block.
Update 2 : Thank you all for your help! I accepted the first answer about intern() ing Strings, which solved my original problem - when multiple threads entered synchronized blocks, where I thought they shouldn't, because the key value has the same value.
As others have pointed out, using intern() for such a purpose and synchronizing on these lines really turns out to be a bad idea - when running JMeter tests against webapp to simulate the expected load, I saw the used heap size grow to almost 1 GB in less than 20 minutes.
I am currently using a simple solution to just synchronize the whole method - but I really , for example, code samples provided by martinprobst and MBCook, but since I have about 7 similar getData() methods in this class at present (since it needs about 7 different pieces of data from the web service), I did not want to add almost duplicate logic about getting and releasing locks for each method. But this is definitely very, very valuable information for future use. I think that these are, ultimately, the correct answers to how best to perform an operation such as this thread-safe, and I would give more votes for these answers if I could!