【问题标题】:Java Threading Unexpected BehaviorJava 线程意外行为
【发布时间】:2017-02-01 00:43:19
【问题描述】:

一段时间以来,我们一直在研究线程错误,但不确定这是怎么可能的。下面是我们代码中的一个最小化示例。有一个缓存保存着从数据库中检索到的数据(或者:就本示例而言:“一个冗长的同步操作”)。有一个线程用于重新加载缓存,而其他线程尝试查询缓存。有一段时间缓存为空,等待重新加载。在这段时间内它不应该是可查询的,我们试图通过同步访问缓存的方法来强制执行这一点——读取和写入。但是,如果您运行该课程一段时间,您将在search() 中获得 NPE。这怎么可能?

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)

我们不明白为什么即使代码是同步的也会发生 NPE。如果我们注释掉对runInLoop 的第三次调用(调用cache.clear),我们也不明白为什么NPE 会停止发生。 我们也尝试使用ReentrantReadWriteLock 来实现锁定 - 结果是一样的。

【问题讨论】:

  • 所以在这段代码中缓存对象没有同步。那么 QueryableCache 做同步吗?您可以发布该代码吗? NPE 似乎位于 QueryableCache 的一些内部数据结构上。
  • slowOp() 是否/应该返回一些东西?我在这里什么都不做。
  • 贴出的代码是自给自足的。它运行并抛出 NPE。 QueryableCache 的所有方法都是同步的(在 QueryableCache 实例的内在锁上,只有一个)。您可以使用代码,例如将私有成员添加到 QueryableCache 并在其上进行同步。在我们的测试中,结果相同。
  • @m0skit0 你试过运行它吗?它是一个 Java 类,可以在发布时运行(无依赖关系)。你得到了什么错误?
  • 哦,哇,我的错。我完全错过了代码块上的滚动。对不起。

标签: java multithreading


【解决方案1】:

由于您没有任何更高级的锁定,您可以连续调用clear()search()。这显然会导致 NPE。

调用reload()search() 不会导致问题,因为在重新加载时缓存会被清除,然后在同步块内重建,从而防止在其间执行其他(搜索)操作。

为什么有一个clear() 方法会使cache 处于“坏”状态(search() 甚至不检查)?

【讨论】:

  • 你让我走上了正轨。我们在这里有一个单独的“清除”方法,因为这就是它在实际生产代码中的方式。我虽然在 reload 运行时会发生 NPE - 但如果你运行 clear 然后 search 然后 reload 它实际上很容易发生
  • 添加更高级的锁定没有帮助,正如我所提到的。这只是一个排序问题(clear 然后search)。
【解决方案2】:

如果cache 为空,您必须检查search 方法。否则,如果您之前在 clear 方法中将 cache 设置为 null,则在 search 中对其调用 contains 可能会引发 NullPointerException

【讨论】:

  • 但是由于方法是同步的,所以在执行reload() 时,另一个线程应该不可能进入search()。这不是同步的保证吗?
  • 您期望方法以正确的顺序执行。但这并不能保证,您应该使用与 clear 相同的同步方法重新加载。
  • @radumanolescu 这不是问题所在。对clear 的调用可以直接跟在对search 的连续调用之后。根据您当前应用的同步机制,不能保证在两者之间会发生对 reload 的调用。
  • 还要记住,在这个实现中,clear() 是公开的。您无法控制谁何时调用它。您可能希望在代码中搜索 QueryableCache 之外的访问权限
【解决方案3】:

同步工作正常。

问题在于clear 方法将缓存放入null。而且不能保证reload方法会在search之前被调用。

另外,请注意reload 方法,它不会释放锁。因此,当您等待slowOp 完成时,其他方法无法执行。

【讨论】:

    【解决方案4】:

    "缓存有一段时间为空,等待重新加载。" 这是你的问题:clear 将事物设置为 null,然后返回,释放同步锁,允许其他人访问。 最好使“新”分配原子而不是clear()

    假设 slowOp() 需要为缓存返回数据(private List&lt;Integer&gt; slowOp()) 您在分配之前检索该数据

    ArrayList<Integer> waitingForData = slowOp(); cache = watingForData; 这仅在数据可用后“更新”缓存。赋值是原子操作,在更新引用时没有任何东西可以访问缓存

    【讨论】:

      【解决方案5】:

      三个不同的线程调用缓存的 clear() search() 和 reload() 而没有明确的交错。由于交错不能保证为 clear() 和 search() 线程获得锁的顺序,因此搜索线程可能会在 clear() 线程之后获得对象上的锁。在这种情况下,搜索将导致 NullPointerException。

      您可能必须在搜索对象中检查缓存是否等于 null,并且可能需要在 search() 方法中执行 reload()。这将保证搜索结果或在适用时返回 null。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-12-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-03-21
        • 2023-04-09
        相关资源
        最近更新 更多