0
votes

Scenario: a class uses Jdk1.7 java.util.HashMap with get() and put() being the only methods called. A am trying to avoid synchronization on get() method. The previously synchronized method ClassloaderHashMap.get() can block all of my threads for seconds when a new class has to be loaded. The nature of class loading is that the objects are added to the HashMap and never removed. My application is using 400 threads and 30'000 classes. I can't use ConcurrentHashMap.

/**
 * Class to simulate lock free reads from HashMap in WebClassLoader.
 */
public static class ClassloaderHashMap {
    private final HashMap<String, String> testHashMap = new HashMap<String, String>();

    public String get(String key) {
        if (testHashMap.containsKey(key)) {
            String result = testHashMap.get(key);
            if (result != null) {
                return result;
            }
        }
        // call synchronized method
        return writeAndGet(key);
    }

    private synchronized String writeAndGet(String key) {
        // find and load class by key, for the test scenario simply use value=key
        testHashMap.put(key, key);
        return testHashMap.get(key);
    }
}

Question: Is there a potential danger with this solution?

I successfully tested a multi-threaded scenario with this code:

package alex;

import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

public class PerfTestLockFreeReadHashMap {
    private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
    private static final int KEY_COUNT = 30179; // same number of loaded classes
                                                // as in my app

    private static int NUM_WRITERS = 20;
    private static int NUM_READERS = 400;
    private static long TEST_DURATION_MS = 1000;

    private static final String[] keysArray = new String[KEY_COUNT];
    static {
        for (int i = 0; i < keysArray.length; i++) {
            keysArray[i] = "com.company.SomeClass-" + i;
        }
    }

    /**
     * Class to simulate lock free reads from HashMap in WebClassLoader.
     */
    public static class ClassloaderHashMap {
        private final HashMap<String, String> testHashMap = new HashMap<String, String>();
        private AtomicLong reads = new AtomicLong();
        private AtomicLong nullentries =  new AtomicLong();
        private AtomicLong writes = new AtomicLong();

        public String get(String key) {
            if (testHashMap.containsKey(key)) {
                reads.incrementAndGet();
                String result = testHashMap.get(key);
                if (result != null) {
                    return result;
                } else {
                    nullentries.incrementAndGet();
                }
            }
            // call synchronized method
            return writeAndGet(key);
        }

        public synchronized String writeAndGet(String key) {
            writes.incrementAndGet();
            testHashMap.put(key, key);
            return testHashMap.get(key);
        }

        @Override
        public String toString() {
            return "ClassloaderHashMap [Lock-free reads=" + reads + ", Null entries=" + nullentries +  ", writes=" + writes + "]";
        }

    }

    public static void main(final String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            ClassloaderHashMap classloaderHashMap = new ClassloaderHashMap();
            System.out.println("*** Run - " + i);
            perfRun(classloaderHashMap);
            System.out.println(classloaderHashMap);
        }
        EXECUTOR.shutdown();
    }

    public static void perfRun(final ClassloaderHashMap classloaderHashMap) throws Exception {
        final CyclicBarrier startBarrier = new CyclicBarrier(NUM_READERS + NUM_WRITERS + 1);
        final CountDownLatch finishLatch = new CountDownLatch(NUM_READERS + NUM_WRITERS);
        final AtomicBoolean runningFlag = new AtomicBoolean(true);
        for (int i = 0; i < NUM_WRITERS; i++) {
            EXECUTOR.execute(new WriterRunner(classloaderHashMap, i, runningFlag, startBarrier, finishLatch));
        }
        for (int i = 0; i < NUM_READERS; i++) {
            EXECUTOR.execute(new ReaderRunner(classloaderHashMap, i, runningFlag, startBarrier, finishLatch));
        }
        awaitBarrier(startBarrier);
        Thread.sleep(TEST_DURATION_MS);
        runningFlag.set(false);
        finishLatch.await();
        System.out.format("%d readers %d writers \n", NUM_READERS, NUM_WRITERS);
    }

    public static void awaitBarrier(final CyclicBarrier barrier) {
        try {
            barrier.await();
        } catch (final Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    public static class WriterRunner implements Runnable {
        private final int id;
        private final AtomicBoolean runningFlag;
        private final CyclicBarrier barrier;
        private final CountDownLatch latch;
        private final ClassloaderHashMap classloaderHashMap;

        public WriterRunner(final ClassloaderHashMap classloaderHashMap, final int id, final AtomicBoolean runningFlag, final CyclicBarrier barrier,
                final CountDownLatch latch) {
            this.id = id;
            this.runningFlag = runningFlag;
            this.barrier = barrier;
            this.latch = latch;
            this.classloaderHashMap = classloaderHashMap;
        }

        @Override
        public void run() {
            awaitBarrier(barrier);
            int writeCounter = 0;
            while (runningFlag.get()) {
                String key = writeCounter + keysArray[writeCounter % KEY_COUNT] + id;
                String result = classloaderHashMap.get(key);
                if (result == null) {
                    result = classloaderHashMap.writeAndGet(key);
                }

                if (!key.equals(result)) {
                    throw new RuntimeException(String.format("Got %s instead of %s.\n", result, key));
                }
                ++writeCounter;
            }
            latch.countDown();
        }
    }

    public static class ReaderRunner implements Runnable {
        private final int id;
        private final AtomicBoolean runningFlag;
        private final CyclicBarrier barrier;
        private final CountDownLatch latch;
        private final ClassloaderHashMap classloaderHashMap;

        public ReaderRunner(final ClassloaderHashMap classloaderHashMap, final int id, final AtomicBoolean runningFlag, final CyclicBarrier barrier,
                final CountDownLatch latch) {
            this.id = id;
            this.runningFlag = runningFlag;
            this.barrier = barrier;
            this.latch = latch;
            this.classloaderHashMap = classloaderHashMap;
        }

        @Override
        public void run() {
            awaitBarrier(barrier);
            int readCounter = 0;
            while (runningFlag.get()) {
                String key = keysArray[readCounter % keysArray.length] + "-" + id;
                String result = classloaderHashMap.get(key);
                if (result == null) {
                    result = classloaderHashMap.writeAndGet(key);
                }

                if (!key.equals(result)) {
                    throw new RuntimeException(String.format("Got %s instead of %s.\n", result, key));
                }

                ++readCounter;
            }
            latch.countDown();
        }
    }


}

Sample output - null entry can happen but does not cause an error, the synchronized method is called in this case:

*** Run - 0
400 readers 20 writers
ClassloaderHashMap [Lock-free reads=4288664, Null entries=0, writes=589699]
*** Run - 1
400 readers 20 writers
ClassloaderHashMap [Lock-free reads=4177513, Null entries=0, writes=965519]
*** Run - 2
400 readers 20 writers
ClassloaderHashMap [Lock-free reads=4701346, Null entries=0, writes=971986]
*** Run - 3
400 readers 20 writers
ClassloaderHashMap [Lock-free reads=8181871, Null entries=1, writes=2076311]
*** Run - 4
400 readers 20 writers
ClassloaderHashMap [Lock-free reads=3225071, Null entries=0, writes=616041]
*** Run - 5
400 readers 20 writers
ClassloaderHashMap [Lock-free reads=2923419, Null entries=0, writes=1762663]
*** Run - 6
400 readers 20 writers
ClassloaderHashMap [Lock-free reads=5514584, Null entries=0, writes=1090732]
*** Run - 7
400 readers 20 writers
ClassloaderHashMap [Lock-free reads=4037333, Null entries=0, writes=948106]
*** Run - 8
400 readers 20 writers
ClassloaderHashMap [Lock-free reads=6604630, Null entries=0, writes=750456]
*** Run - 9
400 readers 20 writers
ClassloaderHashMap [Lock-free reads=5263678, Null entries=0, writes=894637]
3
HashMap offers no guarantee of thread safety to start with; so what's the point of your test?fge

3 Answers

2
votes

No, HashMap is not thread-safe. If there is a thread writing to the map, and another thread reading from it, then the reading thread might see the map in an inconsistent state. Of course, this might funtion correctly a long time, but then produce a bug which hard to reproduce and find.

With a synchronized get() method, the problem is that all access to the map becomes synchronized. Thus, when two threads are simultaneously trying to read from the map, one must wait for the other (although simultaneous reading is not a problem). With 400 threads this is indeed likely to cause consirable delays.

The solution to your problem is to use a java.util.concurrent.locks.ReadWriteLock. (Java offers the java.util.concurrent.locks.ReentrantReadWriteLock implementation to this interface.) With this lock you can make sure that any number of threads can have read access to an object simultaneously, but only one thread can write to the map (and if a thread is writing, then no other thread may be reading). Look at the Java API documentation to see how to use such as lock.

0
votes

Yes, there is a potential danger with this solution. It creates memory inconsistency by not guaranteeing the 'happens-before' principal.

Even if the put() method is synchronized, your get() method can return null or old and incorrect value or override the value which was just put by a different thread through put() (also don't know why you want to call put() from get(). Let the get() return null).

If you do not care about the accuracy of the data then you may implement this but it is definitely not recommended solution.

0
votes

Yes there is.

Since you read without synchronization you can see corrupted state. Actually you are trying to build "Double Checked Locking" which will not work, see http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html