首页 > 解决方案 > 用于缓存的 Java 多线程锁定策略

问题描述

我正在设计一个缓存,我可以在其中保留安全价格,计算起来很耗时。计算完成后,我将它们存储在地图中:安全性是关键,价格是价值。简单的解决方案是在ConcurrentHashMap这里使用,但我正在尝试使用多线程程序来理解不同的锁定策略。

在这里,我正在尝试不同的方法来锁定,以防我需要更新进程缓存中的安全性价格(它可以被视为任何实体类)。

第一种方式:这里我试图在我的缓存类中提供锁定,这样就不需要客户端锁定。

第一种方式的问题:即使我需要更新一个证券的价格,我也会锁定所有证券,因为MyCache它是单例并且所有情况(putPrice 和 getPrice 方法调用)都使用相同的锁定实例,因此所有其他正在尝试的线程更新其他证券也在等待锁定,尽管这可以并行完成。

第一种方式的代码:

class Security {
    int secId;
}
// Singleton class MyCache
public class MyCache {
    private static final HashMap<Security, BigDecimal> cache = new HashMap();
    private final static ReadWriteLock lock = new ReentrantReadWriteLock();

    public BigDecimal getPrice(Security security) {
        lock.readLock().lock();
        try {
            BigDecimal price = cache.get(security);
            if (price == null) {
                price = new BigDecimal(-9999.9999);
            }
            return price;
        } finally {
            lock.readLock().unlock();
        }
    }

    public void putPrice(Security security, BigDecimal price) {
        lock.writeLock().lock();
        try{
            cache.put(security, price);
        }finally {
            lock.writeLock().unlock();
        }
    }
}

第二种方式:在这里,我试图获取对安全性的锁定,为此我在MyCache构造函数中使用了安全性(实例控制类)对象。MyCache不像在第一种情况下那样单例。客户端代码需要实例化 MyCache 的新对象,传递 Security 对象。

第二种方式的问题:这里可能我正在增加复杂性,如果应该在 Security 上获取锁为什么不在 Security 类中实现与锁定相关的代码,我们可以在那里提供 getPrice 和 updatePrice 方法并使用临界区来阻止多个线程进入相同同一安全性的时间(实例控制类,一个安全性只有一个对象)。

第二种方式的代码:

class Security {
    private int secId;
    private final static HashMap<Integer, Security> map = new HashMap<>();

    private Security(Integer secId) {
        this.secId = secId;
    }

    public static synchronized Security getSecurity(Integer secId) {
        Security security = map.get(secId);
        if (security == null) {
            security = new Security(secId);
            map.put(secId, security);
        }
        return security;
    }
}

public class MyCache {

    private static final HashMap<Security, BigDecimal> cache = new HashMap();

    private final Security security;

    public MyCache(Security security) {
        this.security = security;
    }

    public BigDecimal getPrice(Security security) {
            synchronized(security) {
                BigDecimal price = cache.get(security);
                if (price == null) {
                    price = new BigDecimal(-9999.9999);
                }
                return price;
            }
    }

    public void putPrice(Security security, BigDecimal price) {
        synchronized (security){
            cache.put(security, price);
        }
    }
}

标签: javamultithreading

解决方案


您遇到过简单并发结构的问题,即更新结构需要您锁定整个结构,无论使用什么密钥。这显然会损害并发性。您的缓存必须可供所有线程访问,因此锁定它会阻止所有其他尝试访问该结构的线程。

这样做的原因是添加或删除条目可能会导致映射中的内部结构被修改,从而影响其他键。所以你必须锁定整个结构。

您的第一个解决方案将起作用,但与将地图包装在 a 中相同SynchronizedMap(可能更糟)。

您的第二个解决方案有很多错误,但通常它不会工作,因为它不会锁定地图。

有两种前进方式:

  1. 如果您事先知道所有证券是什么,您可以预先构建包含所有已知密钥的缓存。我相信您可以获取和放置,并且不需要同步,因为您只会覆盖现有的地图条目。如果您真的想确定,您可以创建一个 Price 类作为地图中的值项,其中包含您可以修改的价格,然后使用 Price 对象预加载缓存,然后修改 Price 对象中的价格字段。您将永远不必在初始加载后执行 get 和 put。

  2. 如果您事先不知道所有键或者您不想预加载缓存,请使用ConcurrentHashMap. ConcurrentHashMap确实是同步的,但是它在内部创建了多个段,并且有巧妙的策略来划分一组键,这样它就不必锁定整个映射,只锁定一个段。这意味着很有可能避免缓存中的争用。手动输入状态读取通常不会阻塞,并且可以为构造函数指定并发值以控制预期的并发线程数。

并发哈希映射


推荐阅读