简体   繁体   English

Java线程意外行为

[英]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() . 然而,如果你运行这个类一段时间,你将在search()获得NPE。 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". Java文档声明“同一个对象上的两个同步方法的调用不可能交错。当一个线程正在为一个对象执行同步方法时,所有其他线程为同一个对象块调用同步方法(暂停执行)直到第一个线程完成对象“。

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. 我们不明白为什么即使代码同步也会发生NPE。 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 ). 如果我们注释掉第三次调用runInLoop (执行cache.clear ),我们也不明白为什么NPE会停止发生。 We have also tried to implement locking using a ReentrantReadWriteLock - and the result is the same. 我们还尝试使用ReentrantReadWriteLock实现锁定 - 结果是一样的。

You have to check in the search method if cache is null. 如果cache为空,则必须检入search方法。 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. 否则,在以前在clear -method中将cache设置为null的情况下,在search调用contains NullPointerException

Since you don't have any more advanced locking, you can call clear() and search() consecutively. 由于您没有任何更高级的锁定,因此可以连续调用clear()search() That will obviously cause a NPE. 这显然会导致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. 调用reload()search()不会导致问题,因为在重新加载时,缓存会在同步块内被清除,然后重建,从而阻止在其间执行其他(搜索)操作。

Why is there a clear() method that will leave cache in a "bad" state (which search() doesn't even check for)? 为什么有一个clear()方法会使cache处于“坏”状态( search()甚至不检查)?

"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. 这是你的问题: clear将items设置为null,然后返回,释放同步锁,允许其他人访问。 It would be better to make the "new" assignment atomic and not clear() . 最好将“新”赋值设为原子而不是clear()

Assuming that slowOp() needs to return data for the cache ( private List<Integer> slowOp()) you retrieve that data before assigning it 假设slowOp()需要返回缓存的数据( private List<Integer> slowOp()) ,则在分配数据之前检索该数据

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 . 问题是clear方法将cache放到null And there is no guarantee that reload method will be called before search . 并且无法保证在search之前调用reload方法。

Also, note that the method reload , it's not releasing the lock. 另外,请注意该方法reload ,它不会释放锁定。 So, when you are waiting for the slowOp to finish, the other methods can't execute. 因此,当您等待slowOp完成时,其他方法无法执行。

Three different threads invoking clear() search() and reload() of the cache without a definitive interleaving. 三个不同的线程调用高速缓存的clear()search()和reload()而没有明确的交错。 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. 由于交错不保​​证为clear()和search()线程获取锁定序列,因此搜索线程可能有机会在clear()线程之后获取对象的锁定。 In that case the search would result in the NullPointerException. 在这种情况下,搜索将导致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. 您可能必须在搜索对象中检查等于null的缓存,并且可以在search()方法中执行reload()。 This would gurantee the search result or return null as applicable. 这将保证搜索结果或返回null(如果适用)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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