【问题标题】:Java thread safe caching, and return old cach if getting new is in progressJava线程安全缓存,如果正在获取新缓存,则返回旧缓存
【发布时间】:2015-07-10 10:28:55
【问题描述】:

我不得不涉足缓存和多线程(每个请求线程),而且我是该领域的绝对初学者,所以任何帮助将不胜感激

我的要求是:

  • 缓存一个具有以太间隔刷新或用户刷新的单个大对象
  • 由于检索对象数据非常耗时,因此使其成为线程安全的
  • 检索对象数据时返回“旧数据”,直到有新数据可用
  • 优化

在 SO 和其他一些用户的帮助下,我有了这台 ATM:

** 根据 Sandeep 和 Kayaman 的建议编辑 **

public enum MyClass
{
    INSTANCE;

    // caching field
    private CachedObject cached = null;

    private AtomicLong lastVisistToDB = new AtomicLong();
    private long refreshInterval = 1000 * 60 * 5;

    private CachedObject createCachedObject()
    {
        return new CachedObject();
    }

    public CachedObject getCachedObject()
    {
        if( ( System.currentTimeMillis() - this.lastVisistToDB.get() ) > this.refreshInterval)
        {
            synchronized( this.cached )
            {
                if( ( System.currentTimeMillis() - this.lastVisistToDB.get() ) > this.refreshInterval)
                {
                    this.refreshCachedObject();
                }
            }
        }

        return this.cached;
    }

    public void refreshCachedObject()
    {
        // This is to prevent threads waiting on synchronized from re-refreshing the object     
        this.lastVisistToDB.set(System.currentTimeMillis());

        new Thread() 
        {
            public void run() 
            {
                createCachedObject();
                // Update the actual refresh time
                lastVisistToDB.set(System.currentTimeMillis());  
            }
        }.start();
    }
}

在我看来,我的代码满足了上述所有书面要求。 (但我不确定)

随着代码很快进入第三方分析,我非常感谢任何关于代码性能和盲点的意见

感谢您的帮助。

编辑:VanOekel 的答案是解决方案,因为我的代码(根据 Sandeep 和 Kayaman 的建议编辑)没有考虑用户触发的 refresh() 在这个多线程环境中的影响

【问题讨论】:

  • 抱歉,这听起来很迂腐,但拼写为“caching”和“cache”。虽然从 java 代码中获得旧钱的想法让我很开心:)
  • @Dragondraikk 我很抱歉拼写错误,我在缓存方面太新了,我什至无法拼写它...我已修复它:)
  • 除非你要去裸体度假村,否则你可能还想避免说bare with me..

标签: java multithreading performance


【解决方案1】:

代替 Sandeep 提出的 DCL,我会使用枚举单例模式,因为它是当今惰性初始化单例的最佳方式(并且看起来比 DCL 更好)。

使用了很多不必要的变量和代码,我会简化很多。

private static Object cachedObject;
private AtomicLong lastTime = new AtomicLong();
private long refreshPeriod = 1000;

public Object get() {

    if(System.currentTimeMillis() - lastTime.get() > refreshPeriod) {
        synchronized(cachedObject) {
            if(System.currentTimeMillis() - lastTime.get() > refreshPeriod) {
                lastTime.set(System.currentTimeMillis());    // This is to prevent threads waiting on synchronized from re-refreshing the object
                new Thread() {
                    public void run() {
                        cachedObject = refreshObject();  // Get from DB
                        lastTime.set(System.currentTimeMillis());  // Update the actual refresh time
                    }
                }.start();
            }
        }
    }
    return cachedObject;
}

Speedwise 仍然可以改进一点,但减少了很多不必要的复杂性。可以删除对System.currentTimeMillis() 的重复调用,以及两次设置lastTime。但是,让我们从这个开始吧。

【讨论】:

  • 我已按照您的建议编辑了上面的代码。除了枚举单例模式。 ATM 我只是在浏览该模式的初始化时间。以及它是否达到目的
  • 如果你正在创建一个惰性初始化单例,没有理由不使用枚举模式。
  • 是的,现在明白了,我只是想浏览双重检查与枚举单例模式,看看有什么区别(除了代码可读性explained 之外基本上没有)
  • 代码可读性很重要。往往比任何轻微的性能影响都重要,尽管不幸的是,许多经验不足的开发人员倾向于将精力投入到这方面(请参阅所有 what's the most efficient way to <do something completely irrelevant> 帖子)。
  • 嘿嘿,是的,不幸的是我是less experienced developerswhat's the most efficient way to <do something completely irrelevant>。因为我几乎总是独自处理项目,所以我在掌握代码可读性概念方面存在问题(当没有人告诉你这不可读时,这很难)
【解决方案2】:

您应该在getInstance() 中进行双重检查锁定。

此外,您可能希望只保留一个易失性缓存对象,并且在getAndRefreshCashedObject() 中,无论何时刷新,您都可以计算新数据,并以syncronized 的方式将其分配给您的缓存对象有。 这样,代码可能看起来更小,并且不需要维护 loadInProgress、oldCached 变量

【讨论】:

  • 我已经按照建议实施了double checked locking(我已经编辑了我的问题)但不幸的是我不明白你的第二个建议......我的问题是new CachedObject()是非常昂贵且冗长的操作,而且我不能只锁定其他线程,直到新数据被加载...这就是为什么我采用“chached”、“oldCached”数据方法
  • 你不需要停止任何事情。继续将数据加载到临时局部变量中,同时,您可以响应来自 Cache 对象的所有调用。完成临时缓存创建后,将其引用分配给类变量缓存。确保类变量是可变的,以帮助传播到所有线程。
  • 哦,现在我明白了,谢谢。按照你的建议做了,并根据 Kayaman 的建议进行了一些调整
【解决方案3】:

考虑到用户触发的“随机”刷新,我得出了一个稍微不同的解决方案。另外,我认为第一次提取应该等待缓存被填充(即等待第一个缓存对象被创建)。最后,应该有一些(单元)测试来验证缓存是否按预期工作并且是线程安全的。

首先是缓存实现:

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

// http://stackoverflow.com/q/31338509/3080094
public enum DbCachedObject {

    INSTANCE;

    private final CountDownLatch initLock = new CountDownLatch(1);
    private final Object refreshLock = new Object();
    private final AtomicReference<CachedObject> cachedInstance = new AtomicReference<CachedObject>();
    private final AtomicLong lastUpdate = new AtomicLong();
    private volatile boolean refreshing;
    private long cachePeriodMs = 1000L; // make this an AtomicLong if it can be updated

    public CachedObject get() {

        CachedObject o = cachedInstance.get();
        if (o == null || isCacheOutdated()) {
            updateCache();
            if (o == null) {
                awaitInit();
                o = cachedInstance.get();
            }
        }
        return o;
    }

    public void refresh() {
        updateCache();
    }

    private boolean isCacheOutdated() {
        return (System.currentTimeMillis() - lastUpdate.get() > cachePeriodMs);
    }

    private void updateCache() {

        synchronized (refreshLock) {
            // prevent users from refreshing while an update is already in progress
            if (refreshing) {
                return;
            }
            refreshing = true;
            // prevent other threads from calling this method again
            lastUpdate.set(System.currentTimeMillis());
        }
        new Thread() {
            @Override 
            public void run() {
                try {
                    cachedInstance.set(getFromDb());
                    // set the 'real' last update time
                    lastUpdate.set(System.currentTimeMillis());
                    initLock.countDown();
                } finally {
                    // make sure refreshing is set to false, even in case of error
                    refreshing = false;
                }
            }
        }.start();
    }

    private boolean awaitInit() {

        boolean initialized = false;
        try {
            // assume cache-period is longer as the time it takes to create the cached object 
            initialized = initLock.await(cachePeriodMs, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return initialized;
    }

    private CachedObject getFromDb() {
        // dummy call, no db is involved
        return new CachedObject();
    }

    public long getCachePeriodMs() {
        return cachePeriodMs;
    }

}

使用测试缓存实现的主方法第二个缓存对象:

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class CachedObject {

    private static final AtomicInteger createCount = new AtomicInteger();
    static final long createTimeMs = 100L;

    private final int instanceNumber = createCount.incrementAndGet();

    public CachedObject() {
        println("Creating cached object " + instanceNumber);
        try {
            Thread.sleep(createTimeMs);
        } catch (Exception ignored) {}
        println("Cached object " + instanceNumber + " created");
    }

    public int getInstanceNumber() {
        return instanceNumber;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "-" + getInstanceNumber();
    }

    private static final long startTime = System.currentTimeMillis();

    /**
     * Test the use of DbCachedObject.
     */
    public static void main(String[] args) {

        ThreadPoolExecutor tp = (ThreadPoolExecutor) Executors.newCachedThreadPool();
        final int tcount = 2; // amount of tasks running in paralllel
        final long threadStartGracePeriodMs = 50L; // starting runnables takes time
        try {
            // verify first calls wait for initialization of first cached object
            fetchCacheTasks(tp, tcount, createTimeMs + threadStartGracePeriodMs);
            // verify immediate return of cached object
            CachedObject o = DbCachedObject.INSTANCE.get();
            println("Cached: " + o);

            // wait for refresh-period
            Thread.sleep(DbCachedObject.INSTANCE.getCachePeriodMs() + 1);
            // trigger update
            o = DbCachedObject.INSTANCE.get();
            println("Triggered update for " + o);
            // wait for update to complete
            Thread.sleep(createTimeMs + 1);
            // verify updated cached object is returned
            fetchCacheTasks(tp, tcount, threadStartGracePeriodMs);

            // trigger update
            DbCachedObject.INSTANCE.refresh();
            // wait for update to complete
            Thread.sleep(createTimeMs + 1);
            println("Refreshed: " + DbCachedObject.INSTANCE.get());

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            tp.shutdownNow();
        }
    }

    private static void fetchCacheTasks(ThreadPoolExecutor tp, int tasks, long doneWaitTimeMs) throws Exception {

        final CountDownLatch fetchStart = new CountDownLatch(tasks);
        final CountDownLatch fetchDone = new CountDownLatch(tasks);
        // println("Starting " + tasks + " tasks");
        for (int i = 0; i < tasks; i++) {
            final int r = i;
            tp.execute(new Runnable() {
                @Override public void run() {
                    fetchStart.countDown();
                    try { fetchStart.await();} catch (Exception ignored) {}
                    CachedObject o = DbCachedObject.INSTANCE.get();
                    println("Task " + r + " got " + o);
                    fetchDone.countDown();
                }
            });
        }
        println("Awaiting " + tasks + " tasks");
        if (!fetchDone.await(doneWaitTimeMs, TimeUnit.MILLISECONDS)) {
            throw new RuntimeException("Fetch cached object tasks incomplete.");
        }
    }

    private static void println(String msg) {
        System.out.println((System.currentTimeMillis() - startTime) + " "  + msg);
    }
}

main-method 中的测试需要人眼来验证结果,但它们应该为单元测试提供足够的输入。一旦单元测试更加细化,缓存实现可能也需要一些收尾工作。

【讨论】:

  • 我什至没有考虑用户的影响,调用updateCache() 及其影响......抱歉回复晚了,但不幸的是我不得不谷歌搜索你的大部分模式和类再次感谢您提供大量代码和解释 :-)
  • @IvanPavić 很高兴听到您花时间学习更多“交易技巧”,我很高兴能提供帮助。
猜你喜欢
  • 1970-01-01
  • 2021-07-20
  • 2015-05-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-15
  • 2011-03-24
  • 1970-01-01
相关资源
最近更新 更多