Fasterj

|home |articles |cartoons |site map |contact us |
Tools: | GC log analysers| Multi-tenancy tools| Books| SizeOf| Thread analysers|

Our valued sponsors who help make this site possible
Discover the fastest Java Multi-Model DBMS with an Apache 2 License 

Using SharedHashMap - Concurrency handling and thread safety

JProfiler
Get rid of your performance problems and memory leaks!


Java Performance Tuning, 2nd ed
The classic and most comprehensive book on tuning Java

Java Performance Tuning Newsletter
Your source of Java performance news. Subscribe now!
Enter email:


OrientDB
Discover the fastest Java Multi-Model DBMS with an Apache 2 License


JProfiler
Get rid of your performance problems and memory leaks!



1 - Using SharedHashMap | 2 - "no-copy" mode | 3 - Concurrency handling and thread safety | 4 - Appendix: interfaces supported | View All

In this article Jack Shirazi and Peter Lawrey give a worked example of using SharedHashMap, a high performance persisted off-heap hash map, shareable across processes.
Published March 2014, Authors Jack Shirazi and Peter Lawrey

Concurrency handling and thread safety

So far, we've handled concurrency naively, we've just retried on accessing and updating the map, and assumed the data accesses and updates are atomic so we haven't handled that in any special way. This is of course wrong, though for the examples so far, it's massively unlikely you'd see any issues. But naturally we should handle the concurrency correctly.

So what do we need to do? Well accesses and updates to the shared map itself are thread safe - just like ConcurrentHashMap - and even safe across processes so if you were just using SharedHashMap as a map, you can use it similarly to how you would use ConcurrentHashMap.

But the objects you retrieve from the map are not themselves necessarily thread safe, and certainly are not multi-process thread safe, so we need to add some concurrency control. Our "shared copy" examples SHMTest1.java, SHMTest2.java, SHMTest3.java only use object copies from the map, and these copies are local to each thread, so in fact they need no changes at all to their implementations, they are fully concurrency safe. For these implementations, the SharedHashMap handles all the concurrency conflicts correctly, and our "retry updates" strategy works fine in ensuring that the data in the shared map is always correct and non-corrupt. The data objects are not shared across threads, so need no further support, though if they were shared across threads in any one process you would need to add normal thread safety handling to those objects (they cannot be shared across processes because they are copied from the map into the process on access, so no need to consider multi-process concurrency issues for them).

But the last "no-copy" example SHMTest4.java uses a direct reference data object and, apart from the initial access into the map, all our updates and accesses are handled by that direct reference data object. This object is not threadsafe.

If we were using the SharedHashMap only within one process (say to benefit from the off-heap storage, and not for use from multiple processes) then you could use any thread-safety mechanisms that you are already familiar with to handle concurrency. But SharedHashMap supports additional thread-safety mechanisms which handle multi-process concurrency, and also gives you access to compare-and-swap (CAS) capability on the updates to the direct reference data object.

How do we use this? It's actually quite straightforward, the data interface just needs a few more method interfaces added (the full set of concurrency methods supported are illustrated here)

	/**returns true if the CAS operation succeeds. Success means
	 * the value of MaxNumberOfProcessesAllowed was "expected"
	 * at the start of the operation, and "value" at the end
         */
	boolean compareAndSwapMaxNumberOfProcessesAllowed(int expected, int value);

	/**returns true if the lock "Timelock" is acquired within "nanos" nanoseconds
         */
	boolean tryLockNanosTimelock(long nanos);

	/**unlocks the lock "Timelock", throws IllegalMonitorStateException
	 * if the lock "Timelock" wasn't locked when executed
         */
	void unlockTimelock() throws IllegalMonitorStateException;

With these methods, we now have the capability to apply a CAS operation on the MaxNumberOfProcessesAllowed field; and separately the capability to lock any code sections - here we've created a single lock called Timelock (we can have more locks, but one is enough for this example).

Now the correct code for creating/accessing the direct reference data object, using a CAS operation, becomes:

	SHMTest5Data data = DataValueClasses.newDirectReference(SHMTest5Data.class);
	theSharedMap.acquireUsing("whatever", data);
	if (data.getMaxNumberOfProcessesAllowed() != 2) {
		if (data.compareAndSwapMaxNumberOfProcessesAllowed(0, 2)){
			//everything is good
		} else {
			if (data.getMaxNumberOfProcessesAllowed() != 2) {
				System.out.println(
					"Exiting: Incorrect configuration found, expected 2 slots, instead found "
					+ data.getMaxNumberOfProcessesAllowed());
				System.exit(0);
			}
		}
	}

I.e. we've created the direct reference data object exactly as before, but after we obtain the handle to the object, we use standard CAS operational procedure to update the object. The code assumes that either the value is already 2 because the data object was created or updated previously in another process, or it's 0 because it was just created in the acquireUsing() call. If it's any other value, there's a configuration error (e.g. another process created the data object with the wrong value), and we just exit.

For the "time" array, we cannot use a CAS operation, since we need to iterate the array, so we'll need to wrap that with a lock. The code needs to expand quite a bit to handle lock management, here's the access to copy the array:

	long[] times1 = new long[data.getMaxNumberOfProcessesAllowed()];
	boolean locked = false;
	//try for up to 1 second to get the lock
	for (int i = 0; i < 1000000; i++) {
		if (data.tryLockNanosTimelock(1000L)) {
			locked = true;
			break;
		}
	}
	if (!locked){
		System.out.println("Unable to acquire a lock on the time array - exiting");
		System.exit(0);
			
	}
	try{
		//we've got the lock, now copy the array
		for (int i = 0; i < times1.length; i++) {
			times1[i] = data.getTimeAt(i);
		}
	} finally {
		//and release the lock
		try {
			data.unlockTimelock();
		} catch (IllegalMonitorStateException e) {
			//odd, but we'll be unlocked either way
			System.out.println("Unexpected state: "+e);
			e.printStackTrace();
		}
	}

Note that all access and update to the "time" array and any element of the array now needs to be protected by the lock for correct concurrency handling, otherwise in the worst case you could end up with corrupt data. The lock "Timelock" handles locking across the processes as well as threads within the same process, so this is now the full solution. The full implementation is available in SHMTest5.java.


1 - Using SharedHashMap | 2 - "no-copy" mode | 3 - Concurrency handling and thread safety | 4 - Appendix: interfaces supported | View All


Last Updated: 2017-08-30
Copyright © 2007-2017 Fasterj.com. All Rights Reserved.
All trademarks and registered trademarks appearing on Fasterj.com are the property of their respective owners.
URL: http://www.fasterj.com/articles/sharedhashmap1c.shtml
RSS Feed: http://www.JavaPerformanceTuning.com/newsletters.rss
Trouble with this page? Please contact us