简体   繁体   中英

Java Threading Unexpected Behavior

We have been looking at a threading error for a while and are not sure how this is possible. Below is a minimized example from our code. There is a cache holding data retrieved from a database (or: "a lengthy synchronous operation", as far as this example is concerned). There is a thread for reloading the cache, while other threads try to query the cache. There is a period of time when the cache is null, waiting to be reloaded. It should not be queryable during this time, and we tried to enforce this by synchronizing the methods that access the cache - both for reading and writing. Yet if you run this class for a while, you will get NPEs in search() . How is this possible?

Java docs state that "it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object".

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CacheMultithreading01 {
    private long dt = 1000L;

    public static void main(String[] args) {
        CacheMultithreading01 cm = new CacheMultithreading01();
        cm.demonstrateProblem();
    }

    void demonstrateProblem() {
        QueryableCache cache = new QueryableCache();
        runInLoop("Reload", new Runnable() {
            @Override
            public void run() {
                cache.reload();
            }
        });
        runInLoop("Search", new Runnable() {
            @Override
            public void run() {
                cache.search(2);
            }
        });
        // If the third "runInLoop" is commented out, no NPEs
        runInLoop("_Clear", new Runnable() {
            @Override
            public void run() {
                cache.clear();
            }
        });
    }

    void runInLoop(String threadName, Runnable r) {
        new Thread(new Runnable() {
            @Override
            public synchronized void run() {
                while (true) {
                    try {
                        r.run();
                    } catch (Exception e) {
                        log("Error");
                        e.printStackTrace();
                    }
                }
            }
        }, threadName).start();
    }

    void log(String s) {
        System.out.format("%d %s %s\n", System.currentTimeMillis(), Thread
                .currentThread().getName(), s);
    }

    class QueryableCache {
        private List<Integer> cache = new ArrayList<>();

        public synchronized void reload() {
            clear();
            slowOp(); // simulate retrieval from database
            cache = new ArrayList<>(Arrays.asList(1, 2, 3));
        }

        public synchronized void clear() {
            cache = null;
        }

        public synchronized Integer search(Integer element) {
            if (cache.contains(element))
                return element;
            else
                return null;
        }

        private void slowOp() {
            try {
                Thread.sleep(dt);
            } catch (InterruptedException e) {
            }
        }
    }
}
//java.lang.NullPointerException
//at examples.multithreading.cache.CacheMultithreading01$QueryableCache.search(CacheMultithreading01.java:73)
//at examples.multithreading.cache.CacheMultithreading01$2.run(CacheMultithreading01.java:26)
//at examples.multithreading.cache.CacheMultithreading01$4.run(CacheMultithreading01.java:44)
//at java.lang.Thread.run(Thread.java:745)

We do not understand why the NPEs can happen even though the code is synchronized. We also do not understand why the NPEs stop happening if we comment out the third call to runInLoop (the one that does cache.clear ). We have also tried to implement locking using a ReentrantReadWriteLock - and the result is the same.

You have to check in the search method if cache is null. Otherwise calling contains on it in search can throw a NullPointerException in the case that you have previously set cache to null in the clear -method.

Since you don't have any more advanced locking, you can call clear() and search() consecutively. That will obviously cause a NPE.

Calling reload() and search() won't cause problems, since in reload the cache is cleared, then rebuilt, inside a synchronized block, preventing other (search) operations from being executed in between.

Why is there a clear() method that will leave cache in a "bad" state (which search() doesn't even check for)?

"There is a period of time when the cache is null, waiting to be reloaded." This is your problem: clear sets things to null, and then returns, releasing the synchronization lock, allowing someone else to access. It would be better to make the "new" assignment atomic and not clear() .

Assuming that slowOp() needs to return data for the cache ( private List<Integer> slowOp()) you retrieve that data before assigning it

ArrayList<Integer> waitingForData = slowOp(); cache = watingForData; This 'updates' the cache only after the data is available. Assignment is an atomic operation, nothing can access cache while the reference is being updated.

Synchronization is working as correctly.

The problem is that the method clear puts cache to null . And there is no guarantee that reload method will be called before search .

Also, note that the method reload , it's not releasing the lock. So, when you are waiting for the slowOp to finish, the other methods can't execute.

Three different threads invoking clear() search() and reload() of the cache without a definitive interleaving. Since the interleaving doesn't gurantees the the sequence of lock being obtained for the clear() and search() threads there could be chances where the search thread is getting the lock over the object just after the clear() thread. In that case the search would result in the NullPointerException.

You may have to check for the cache equal to null in the search object and may be do a reload() from within the search() method. This would gurantee the search result or return null as applicable.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM