Should I acquire a lock on Properties first before

2019-07-16 14:12发布

问题:

The java class Properties is a thread-safe Class as per it's documentation:

This class is thread-safe: multiple threads can share a single Properties object without the need for external synchronization.

Because of that reason, I have the habit of maintaining my Properties in a HashMap implementation of Map, which is not thread-safe, but much more lightweight. More specifically, implementing thread-safety requires additional locking mechanisms, which will have an impact on performance. I can easily avoid this by simply segregating my property initialization in a static class initializer, which will guarantee it's completion before I use any get calls in instance methods of the same class.

This is so far was just a narrative that leads me to the actual question. Eventually I need to revert to API's that can only accept 'Properties' as a Parameter, an example being DriverManager.getConnection(String,Properties). I need to convert my Map into Properties.

So a first attempt would look like something like this:

  Properties properties = new Properties();
  this.propertyMap.forEach((k,v)->{properties.setProperty(k, v);});
  connection = DriverManager.getConnection(url, properties);

Obvious problem, or maybe not so obvious as I avoided using an actual for-loop, is that I use a repeated call to Properties.setProperty. If Properties is truly thread safe, then that must mean that each call to setProperty is synchronized and has individual lock/unlock mechanisms added to it.

Would it not be better in such a case that I manually lock the entire Properties instance first as in code below?

Connection connection;
Properties properties = new Properties();
synchronized (properties) {
  // Properties is implemented as thread-safe. As it adds
  // additional thread locking, it's use is localized to just
  // here to avoid consequential performance issues. We will
  // do a last minute conversion from Map to Properties right here.
  this.propertyMap.forEach((k,v)->{properties.setProperty(k, v);});
  connection = DriverManager.getConnection(url, properties);
}

One issue I was possibly expecting is that both manual lock on properties, and the individual calls to setProperties might have caused a deadlock, but it seems to run fine.

回答1:

I have made invalid conclusions based on improper benchmarking. Even though I have attempted some type of warm-up, It was never really enough.

Only by the third iteration (once the iteration was added), which was using a full dataset, did the measurements level off, and came within range of each other so they could be called same with insignificant differences.

Amended Conclusion

Conclusion is now that the methods are really pretty much irrelevant. I can still see that HashMap is overall a bit better performing than Properties but not enough to make me choose performance over readability.

As per the suggestion of @Holger, I would propose to just use the Properties.putAll(other) method. It is fast, and performs about as well as the others.

However, if you do use this in a multithreaded environment, I would suggest using HashMap, and carefully consider concurrent access. Usually that is easily accomplishable without the need of overly locking/synchronize.

I think that was a good lesson learned.

Below are a bit more normalized measurements. I cheated a little by eyeballing and picking a sample without outliers instead of trying to average/median over multiple samples. Just for avoiding the complexity (and additional work) of doing so, or having to acquire an external tool.

+----------------------------------+----------+--------+
|               Name               | Unlocked | Locked |
+----------------------------------+----------+--------+
| putAll->Properties               | 1,644    | 1,429  |
| forEach->setProperty->Properties | 1,474    | 1,740  |
| stream->setProperty->Properties  | 1,484    | 1,735  |
| loop->setProperty->Properties    | 2,022    | 1,606  |
| forEach->put->Properties         | 1,590    | 1,411  |
| stream->put->Properties          | 1,538    | 1,514  |
| loop->put->Properties            | 1,380    | 1,666  |
| putAll->Map                      | 1,380    | 509    |
| forEach->put->Map                | 935      | 915    |
| stream->put->Map                 | 927      | 888    |
| loop->put->Map                   | 880      | 1,015  |
+----------------------------------+----------+--------+

Methodology

The methods for testing, is in the same order as in the table above, with p being a Properties and m a Map as a target:

  p.putAll(SOURCEMAP);
  SOURCEMAP.forEach(p::setProperty);
  SOURCEMAP.entrySet().stream().forEach((e)->{p.setProperty(e.getKey(), e.getValue());});
  for (Map.Entry<String,String> e:SOURCEMAP.entrySet()) p.setProperty(e.getKey(), e.getValue());
  SOURCEMAP.forEach(p::put);
  SOURCEMAP.entrySet().stream().forEach((e)->{p.put(e.getKey(), e.getValue());});
  for (Map.Entry<String,String> e:SOURCEMAP.entrySet()) p.put(e.getKey(), e.getValue());
  m.putAll(SOURCEMAP);
  SOURCEMAP.forEach(m::put);
  SOURCEMAP.entrySet().stream().forEach((e)->{m.put(e.getKey(), e.getValue());});
  for (Map.Entry<String,String> e:SOURCEMAP.entrySet()) m.put(e.getKey(), e.getValue());

The full source code is available at GitHub: http://github.com/jdesmet/properties-benchmarking.