高效的缓存同步

时间:2010-10-25 18:00:14

标签: java multithreading servlets synchronization locking

考虑一下

public Object doGet() {
    return getResource();
}

private Object getResource() {
    synchronized (lock) {
        if (cachedResourceIsStale()) {
            downloadNewVersionOfResource();
        }
    }

    return resource;
}

假设doGet将同时执行,并且很多,并且下载新版本的资源需要一段时间,是否有更有效的方法在getResource中进行同步?我知道读/写锁,但我不认为它们可以在这里应用。

为什么要同步?如果缓存过时,所有在第一个缓存仍在刷新时访问资源的线程将执行自己的刷新。在这引起的其他问题中,它几乎没有效率。

正如BalusC在评论中提到的那样,我目前正在servlet中面对这个问题,但我对通用答案感到满意,因为谁知道在什么情况下我会再遇到它。

2 个答案:

答案 0 :(得分:4)

<强>假设

  1. 有效意味着doGet()应尽快完成
  2. cachedPageIsStale()根本没有时间
  3. downloadNewVersionOfResource()需要一点时间
  4. <强>答案

    Sychronizing减少了网络负载,因为只有一个线程在资源到期时获取资源。此外,它不会过度延迟其他线程的处理 - 因为VM不包含线程可能返回的当前快照,它们必须阻塞,并且没有理由另外的并发downloadNewVersionOfResource()将更快完成(I由于网络带宽争用,预计会出现相反的情况。

    因此,同步是好的,并且在带宽消耗和响应时间方面是最佳的。 (与I / O等待相比,同步的CPU开销非常小) - 假设在调用doGet()时,当前版本的资源可能不可用;如果您的服务器始终拥有该资源的当前版本,则可以立即将其发回。 (您可能有一个后台线程在旧版本到期之前下载新版本。)

    <强> PS

    您尚未显示任何错误处理。您必须决定是否将downloadNewVersionOfResource()抛出的异常传播给您的调用者,或者继续提供旧版本的资源。

    修改

    所以?假设你有100个连接工作者,检查资源是否过时需要1微秒,资源不是陈旧的,服务需要一秒钟。然后,平均而言,100 * 10 ^ -6 / 1 = 0.0001个线程正试图获得锁定。几乎没有任何争论。获取未锁定锁的开销大约为10 ^ -8秒。当网络导致毫秒延迟时,没有必要优化已经采取微观的事情。如果您不相信我,请使用微基准进行同步。确实,频繁的,不必要的同步会增加大量开销,并且由于这个原因,同步集合类已被弃用。但那是因为这些方法每次调用的工作量很少,而且同步的相对开销要大得多。我刚刚为以下代码做了一个小的微基准测试:

    synchronized (lock) {
        c++;
    }
    

    在我的笔记本上,在太阳的热点vm中,这需要50纳秒(5 * 10 ^ -8秒),平均超过1000万次执行。这大约是裸增量操作的20倍,因此如果进行大量增量,则同步每个增量将使程序的速度降低一个数量级。但是,如果该方法确实阻塞了I / O,等待1毫秒,那么添加相同的50纳秒会使吞吐量降低0.005%。当然,你有更好的性能调整机会: - )

    这就是为什么你应该在开始优化之前进行测量。它可以防止您花费数小时的时间来节省几纳秒的处理器时间。

答案 1 :(得分:1)

有可能通过使用“锁定条带化”来减少锁争用(从而改善吞吐量) - 基本上,将一个锁分成几个锁,每个锁保护特定的用户组。
棘手的部分是如何弄清楚如何将用户分配到组。最简单的情况是您可以将任何用户的请求分配给任何组。如果您的数据模型要求必须按顺序处理来自一个用户的请求,则必须在用户请求和组之间引入一些映射。这是StripedLock的示例实现:

import java.util.concurrent.locks.ReentrantLock;

/**
 * Striped locks holder, contains array of {@link java.util.concurrent.locks.ReentrantLock}, on which lock/unlock
 * operations are performed. Purpose of this is to decrease lock contention.
 * <p>When client requests lock, it gives an integer argument, from which target lock is derived as follows:
 * index of lock in array equals to <code>id & (locks.length - 1)</code>.
 * Since <code>locks.length</code> is the power of 2, <code>locks.length - 1</code> is string of '1' bits,
 * and this means that all lower bits of argument are taken into account.
 * <p>Number of locks it can hold is bounded: it can be from set {2, 4, 8, 16, 32, 64}.
  */
public class StripedLock {
    private final ReentrantLock[] locks;

    /**
     * Default ctor, creates 16 locks
     */
    public StripedLock() {
        this(4);
    }

    /**
     * Creates array of locks, size of array may be any from set {2, 4, 8, 16, 32, 64} 
     * @param storagePower size of array will be equal to <code>Math.pow(2, storagePower)</code>
     */
    public StripedLock(int storagePower) {
        if (storagePower < 1 || storagePower > 6)
             throw new IllegalArgumentException("storage power must be in [1..6]");
        int lockSize = (int) Math.pow(2, storagePower);
        locks = new ReentrantLock[lockSize];
        for (int i = 0; i < locks.length; i++)
            locks[i] = new ReentrantLock();
    }

    /**
     * Locks lock associated with given id.
     * @param id value, from which lock is derived
     */
    public void lock(int id) {
        getLock(id).lock();
    }

    /**
     * Unlocks lock associated with given id.
     * @param id value, from which lock is derived 
     */
    public void unlock(int id) {
        getLock(id).unlock();
    }

    /**
     * Map function between integer and lock from locks array
     * @param id argument
     * @return lock which is result of function 
     */
    private ReentrantLock getLock(int id) {
        return locks[id & (locks.length - 1)];
    }
}