Map键级别上的Java并发锁定

时间:2015-11-03 14:41:20

标签: java multithreading concurrency concurrenthashmap

有作者通过调用putPrice方法更新价格。读者正在使用getPrice来获取最新价格。 hasChangedMethod返回一个布尔值,用于标识自上次调用getPrice以来价格是否已更改。

我正在寻找最快的解决方案。我试图在关键级别实现线程安全的一致读/写地图。

我认为锁定整个地图可能会导致性能问题,这就是为什么我决定在关键级别上制作它。不幸的是,它没有按预期工作并阻止整个地图。为什么?你能不能帮我弄清楚我在这里做错了什么?

更新

我想我们可以总结两个问题:1。如果有更新过程,我如何提供对其余密钥的免费访问。 2.我如何保证我的方法的原子操作,因为它们需要多次操作读/写。例如getPrice() - 获取价格并更新hasChanged标志。

PriceHolder.java

public final class PriceHolder {

    private ConcurrentMap<String, Price> prices;

    public PriceHolder() {
        this.prices = new ConcurrentHashMap<>();

        //Receive starting prices..
        Price EUR = new Price();
        EUR.setHasChangedSinceLastRead(true);
        EUR.setPrice(new BigDecimal(0));

        Price USD = new Price();
        USD.setHasChangedSinceLastRead(true);
        USD.setPrice(new BigDecimal(0));
        this.prices.put("EUR", EUR);
        this.prices.put("USD", USD);

    }

    /** Called when a price ‘p’ is received for an entity ‘e’ */
    public void putPrice(
            String e,
            BigDecimal p) throws InterruptedException {

            synchronized (prices.get(e)) {
                Price currentPrice = prices.get(e);
                if (currentPrice != null && !currentPrice.getPrice().equals(p)) {
                    currentPrice.setHasChangedSinceLastRead(true);
                    currentPrice.setPrice(p);
                } else {
                    Price newPrice = new Price();
                    newPrice.setHasChangedSinceLastRead(true);
                    newPrice.setPrice(p);
                    prices.put(e, newPrice);
                }
            }
    }

    /** Called to get the latest price for entity ‘e’ */
    public BigDecimal getPrice(String e) {
        Price currentPrice = prices.get(e);
        if(currentPrice != null){
            synchronized (prices.get(e)){
                currentPrice.setHasChangedSinceLastRead(false);
                prices.put(e, currentPrice);
            }
            return currentPrice.getPrice();
        }
        return null;
    }

    /**
     * Called to determine if the price for entity ‘e’ has
     * changed since the last call to getPrice(e).
     */
    public boolean hasPriceChanged(String e) {
        synchronized (prices.get(e)){
            return prices.get(e) != null ? prices.get(e).isHasChangedSinceLastRead() : false;
        }
    }
}

Price.java

public class Price {

    private BigDecimal price;

    public boolean isHasChangedSinceLastRead() {
        return hasChangedSinceLastRead;
    }

    public void setHasChangedSinceLastRead(boolean hasChangedSinceLastRead) {
        this.hasChangedSinceLastRead = hasChangedSinceLastRead;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    private boolean hasChangedSinceLastRead = false;

}

7 个答案:

答案 0 :(得分:1)

这样的东西
class AtomicPriceHolder {

  private volatile BigDecimal value;
  private volatile boolean dirtyFlag;

  public AtomicPriceHolder( BigDecimal initialValue) {
    this.value = initialValue;
    this.dirtyFlag = true;
  }

  public synchronized void updatePrice( BigDecimal newPrice ) {
    if ( this.value.equals( newPrice ) == false) {
      this.value = newPrice;
      this.dirtyFlag = true;
    }
  }

  public boolean isDirty() {
    return this.dirtyFlag;
  }

  public BigDecimal peek() {
    return this.value;
  }

  public synchronized BigDecimal read() {
    this.dirtyFlag = false;
    return this.value;
  }

}

...

public void updatePrice( String id, BigDecimal value ) {

  AtomicPriceHolder holder;
  synchronized( someGlobalSyncObject ) {
    holder = prices.get(id);
    if ( holder == null ) {
      prices.put( id, new AtomicPriceHolder( value ) );
      return;
    }
  }

  holder.updatePrice( value );

}

请注意,它可能没有任何意义,因为价格值的实际原子修改是如此之快,以至于你不能指望从以前解锁地图中获得任何东西。

条件操作&#34;检查它是否在地图中,创建一个新的并且如果不是&#34;必须是原子的,并且应该通过锁定整个地图的那段短暂时间来完成。其他任何东西都需要每个密钥的专用同步对象。这些必须在某处存储和管理,并且必须再次同步对该商店的访问&amp; c。

只需进行粗粒度锁定以确保您具有正确性,然后继续前进。

答案 1 :(得分:1)

  
      
  1. 如果在更新过程中,我如何提供对其余密钥的免费访问。
  2.   

只需使用ConcurrentHashMap即可确保免费访问密钥; get不会引入任何争用,put只会锁定键的子集而不是整个地图。

  
      
  1. 我如何保证我的方法的原子操作,因为它们需要读/写多个操作。
  2.   

为了确保一致性,您需要在某个共享对象上进行同步(或使用其他锁定机制,如ReentrantLock);我建议创建一个ConcurrentHashMap<String, Object>锁定对象,以便你可以这样做:

synchronized (locks.get(e)) { ... }

只需使用new Object()填充地图即可。您使用的模式(锁定在Price对象上)的风险现在是这些对象必须持久且永远不会被替换。通过拥有专用的私有锁集合而不是将值类型重载为锁定机制,可以更容易地实现这一点。

顺便说一句,如果您尝试在Java中进行资金操作,您绝对应该使用Joda-Money库,而不是重新发明轮子。

答案 2 :(得分:1)

ConcurrentMap的使用在很大程度上取决于Java版本。当您使用Java 8或更高版本时,您几乎可以免费获得所有内容:

public final class PriceHolder {

    private ConcurrentMap<String, Price> prices;

    public PriceHolder() {
        this.prices = new ConcurrentHashMap<>();

        //Receive starting prices..
        Price EUR = new Price();
        EUR.setHasChangedSinceLastRead(true);
        EUR.setPrice(BigDecimal.ZERO);

        Price USD = new Price();
        USD.setHasChangedSinceLastRead(true);
        USD.setPrice(BigDecimal.ZERO);
        this.prices.put("EUR", EUR);
        this.prices.put("USD", USD);

    }

    /** Called when a price ‘p’ is received for an entity ‘e’ */
    public void putPrice(String e, BigDecimal p) {
        prices.compute(e, (k, price)-> {
            if(price==null) price=new Price();
            price.setHasChangedSinceLastRead(true);
            price.setPrice(p);
            return price;
        });
    }

    /** Called to get the latest price for entity ‘e’ */
    public BigDecimal getPrice(String e) {
        Price price = prices.computeIfPresent(e, (key, value) -> {
            value.setHasChangedSinceLastRead(false);
            return value;
        });
        return price==null? null: price.getPrice();
    }

    /**
     * Called to determine if the price for entity ‘e’ has
     * changed since the last call to getPrice(e).
     */
    public boolean hasPriceChanged(String e) {
        final Price price = prices.get(e);
        return price!=null && price.isHasChangedSinceLastRead();
    }
}

并发映射上的compute…方法在计算持续时间内锁定受影响的条目,同时让所有其他条目的更新继续进行。对于get中的简单hasPriceChanged访问,只要在方法中只调用一次,就不需要进行额外的同步,即在检查时将结果保存在局部变量中。

在Java 8之前,事情变得更加复杂。在那里,所有ConcurrentMap提供的都是某些原子更新方法,可用于以尝试重复的方式构建更多高级更新方法。

要干净利用它,最好的方法是使值类不可变:

public final class Price {

    private final BigDecimal price;
    private final boolean hasChangedSinceLastRead;

    Price(BigDecimal value, boolean changed) {
      price=value;
      hasChangedSinceLastRead=changed;
    }
    public boolean isHasChangedSinceLastRead() {
        return hasChangedSinceLastRead;
    }
    public BigDecimal getPrice() {
        return price;
    }
}

然后使用它来构建一个反映所需新状态的新对象,并使用putIfAbsentreplace执行原子更新:

public final class PriceHolder {

    private ConcurrentMap<String, Price> prices;

    public PriceHolder() {
        this.prices = new ConcurrentHashMap<>();

        //Receive starting prices..
        Price EUR = new Price(BigDecimal.ZERO, true);
        Price USD = EUR; // we can re-use immutable objects...
        this.prices.put("EUR", EUR);
        this.prices.put("USD", USD);
    }

    /** Called when a price ‘p’ is received for an entity ‘e’ */
    public void putPrice(String e, BigDecimal p) {
      Price old, _new=new Price(p, true);
      do old=prices.get(e);
      while(old==null? prices.putIfAbsent(e,_new)!=null: !prices.replace(e,old,_new));
    }

    /** Called to get the latest price for entity ‘e’ */
    public BigDecimal getPrice(String e) {
        for(;;) {
          Price price = prices.get(e);
          if(price==null) return null;
          if(!price.isHasChangedSinceLastRead()
          || prices.replace(e, price, new Price(price.getPrice(), false)))
            return price.getPrice();
        }
    }

    /**
     * Called to determine if the price for entity ‘e’ has
     * changed since the last call to getPrice(e).
     */
    public boolean hasPriceChanged(String e) {
        final Price price = prices.get(e);
        return price!=null && price.isHasChangedSinceLastRead();
    }
}

答案 3 :(得分:1)

  

hasChangedMethod返回一个布尔值,用于标识自上次调用getPrice以来价格是否已更改。

这是一个有问题的模式,因为hasPriceChanged基本上需要返回不同的每个线程。如果你可以详细说明你实际上想做什么(也就是为什么你认为你需要这种模式),那么可以提供另一种选择。例如,考虑完全取消hasPriceChanged,并简单地将此数据结构视为规范并每次查询其当前值。

那就是说,这就是我如何实现你正在寻找的行为。可能有其他选择,这只是第一次通过。

保持ConcurrentHashMap<String, ThreadLocal<Boolean>>; ThreadLocal将存储每个线程的get调用状态。我还使用了一个单独的私人锁图。

ConcurrentHashMap<String, Price> pricesMap;
ConcurrentHashMap<String, ThreadLocal<Boolean>> seenMap;
ConcurrentHashMap<String, Object> lockMap;

private Object getLock(String key) {
  return lockMap.computeIfAbsent(key, k -> new Object());
}

private ThreadLocal<Boolean> getSeen(String key) {
  return seenMap.computeIfAbsent(e,
      ThreadLocal.<Boolean>withInitial(() -> false));
}

public void putPrice(String e, BigDecimal p) {
  synchronized (getLock(e)) {
    // price has changed, clear the old state to mark all threads unseen
    seenMap.remove(e);
    pricesMap.get(e).setPrice(p);
  }
}

public BigDecimal getPrice(String e) {
  synchronized (getLock(e)) {
    // marks the price seen for this thread
    getSeen(e).set(true);
    BigDecimal price = pricesMap.get(e);
    return price != null ? price.getPrice() : null;
  }
}

public boolean hasPriceChanged(String e) {
  synchronized (getLock(e)) {
    return !getSeen(e).get();
  }
}

请注意,虽然数据结构是线程安全的,但仍存在竞争条件的风险 - 您可以致电hasPriceChanged()然后返回false,之后价格会被其他人更改线。废除此hasPriceChanged()行为可能会简化您的代码。

答案 4 :(得分:0)

我建议使用observable - observer模式。无需重新发明轮子。见Observer and Observable

我还建议调查Condition因为不需要锁定所有读者的完整对象。阅读可以是并发的,但写作不能。

如果一个集合是并发的,那并不意味着它神奇地同步所有东西。它只是保证他们的方法是线程安全的。离开功能范围后,锁定被释放。因为您需要一种更高级的控制同步方法,所以最好自己动手使用普通的HashMap。

一些注意事项:

你过度使用HashMap.get。考虑一次将其存储在变量中。

synchronized (prices.get(e))

这可能会返回null,您应该检查它。不允许在null对象上同步。

prices.put(e, currentPrice);

我不确定这是否有意,但不需要这个动作。见this

答案 5 :(得分:0)

获得所需内容的最佳方法可能是继续使用ConcurrentMap作为地图,并将所有其他同步放在Price类中。这将导致更简单的代码,这在多线程环境中总是非常有价值,以避免细微的错误,同时还实现了同时访问地图和跟踪自上次读取每个货币后是否有写入的目标。 / p>

在Price类中,无论何时设置价格,您还要设置hasChangedSinceLastRead;这两件事一起作为一个应该是原子的操作。无论何时读取价格,您还要清除hasChangedSinceLastRead;你也想成为原子。因此,该类应该只允许这两个操作修改hasChangedSinceLastRead,而不是将逻辑留给其他类,并且应该同步这些方法以确保pricehasChangedSinceLastRead不会因同步而失去同步多线程访问。该课程现在应该是这样的:

public class Price {

    public boolean isHasChangedSinceLastRead() {
        return hasChangedSinceLastRead;
    }

    // setHasChangedSinceLastRead() removed

    public synchronized BigDecimal getPrice() {
        hasChangedSinceLastRead = false;
        return price;
    }

    public synchronized void setPrice(BigDecimal newPrice) {
        if (null != price && price.equals(newPrice) {
            return;
        }
        price = newPrice;
        hasChangedSinceLastRead = true;
    }

    private BigDecimal price;
    private volatile boolean hasChangedSinceLastRead = false;
}

注意,您可以使isHasChangedSinceLastRead()同步,或者hasChangedSinceLastRead volatile;我选择后者,留下isHasChangedSinceLastRead()不同步,因为同步方法需要一个完整的内存屏障,而使变量volatile只需要在读取变量时读取一半内存屏障。

读取hasChangedSinceLastRead需要某种内存障碍的原因是因为同步方法,块和易失性访问仅保证有效的执行顺序 - “之前发生”关系 - 与其他同步方法,块和易失性访问。如果isHasChangedSinceLastRead未同步且变量不是volatile,则不存在“之前发生”关系;在这种情况下isHasChangedSinceLastRead可以返回“true”但调用它的线程可能看不到价格的变化。这是因为如果“之前发生”关系尚未建立,其他线程可能会以相反的顺序看到pricehasChangedSinceLastReadsetPrice()的设置。

现在所有必要的同步都在ConcurrentMap和Price类中,您不再需要在PriceHolder中进行任何同步,并且PriceHolder不再需要担心更新hasChangedSinceLastRead。代码简化为:

public final class PriceHolder {

    private ConcurrentMap<String, Price> prices;

    public PriceHolder() {
        prices = new ConcurrentHashMap<>();

        //Receive starting prices..
        Price EUR = new Price();
        EUR.setPrice(new BigDecimal(0));
        this.prices.put("EUR", EUR);

        Price USD = new Price();
        USD.setPrice(new BigDecimal(0));
        this.prices.put("USD", USD);
    }

    /** Called when a price ‘p’ is received for an entity ‘e’ */
    public void putPrice(
        String e,
        BigDecimal p
    ) throws InterruptedException {
        Price currentPrice = prices.get(e);
        if (null == currentPrice) {
            currentPrice = new Price();
            currentPrice = prices.putIfAbsent(e);
        }
        currentPrice.setPrice(p);
    }

    /** Called to get the latest price for entity ‘e’ */
    public BigDecimal getPrice(String e) {
        Price currentPrice = prices.get(e);
        if (currentPrice != null){
            return currentPrice.getPrice();
        }
        return null;
    }

    /**
     * Called to determine if the price for entity ‘e’ has
     * changed since the last call to getPrice(e).
     */
    public boolean hasPriceChanged(String e) {
        Price currentPrice = prices.get(e);
        return null != currentPrice ? currentPrice.isHasChangedSinceLastRead() : false;
    }
}

答案 6 :(得分:0)

我认为您只能锁定您的特定密钥。

import java.util.HashMap;

public class CustomHashmap<K, V> extends HashMap<K, V>{

    private static final long serialVersionUID = 1L;

    @Override
    public V put(K key, V value) {
        V val = null;
        synchronized (key) {
            val = super.put(key, value);
        }

        return val;
    }

    @Override
    public V get(Object key) {
        return super.get(key);
    }


}