【问题标题】:Multikey map with Long or AtomicLong value in JavaJava中具有Long或AtomicLong值的多键映射
【发布时间】:2017-07-20 00:17:24
【问题描述】:

我需要一个带有AtomicLong 值的多键映射。所以类似于 guavaAtomicLongMap,但它支持多个键。所以我的新地图应该可以这样做:

MultiKeyAtomicLongMap<String, String> map = ...

map.put("a", "A", 1L);
map.put("a", "B", 2L);
map.put("b", "C", 3L);
....
map.get("a", "A");  // This should give me value 1L
map.get("a");  // This should give me both mappings ["A", 1L] and ["B", 2L]

所以上面的代码只是为了解释预期的行为,但并不是我想要的。

基本上我想要的是线程安全的多键映射,其中我的两个键都是String,值是long


编辑: 我可以保留值 Long 而不是 AtomicLong,但我只希望地图是 线程安全的

【问题讨论】:

  • 看起来你需要一个番石榴Table&lt;String, String, AtomicLong&gt;,不是吗?
  • @ControlAltDel 检索单个键并不容易(就像 OP 第二个 get 要求一样)
  • @JohnVint 我认为你的第一条评论算是一个答案......
  • @JohnVint 啊谢谢!
  • @James 是的,那么应该怎么做才能使其线程安全?仅使用 Long 并在外部同步它,我不认为那也很好。

标签: java multithreading collections guava atomic


【解决方案1】:

此答案基于Jon Vint的评论

看起来你需要一个番石榴桌

Guava 表看起来可以满足您的需求,但目前还没有线程安全的实现。部分困难在于您需要管理 Map of Maps,并公开对 Map of values 的访问权限。

但是如果你乐于同步访问你自己的集合,我认为一个番石榴表可以为你提供你想要的功能并且你可以添加线程安全。并为 inc/dec Long 添加您想要的实用程序。

这比你问的要抽象一点,但我认为这可以满足你的需要:

import com.google.common.base.MoreObjects;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Table;
import java.util.Map;
import java.util.function.Function;
import javax.annotation.concurrent.GuardedBy;

/**
 * Provide something like the {@link com.google.common.util.concurrent.AtomicLongMap} but supporting
 * multiple keys.
 *
 * Should be able to put a value using two keys. And retrieve either a precise cell. Or retrieve a
 * collection of values.
 *
 * Created by James on 28/02/2017.
 */
public class SynchronizedMultimap<Row, Column, Value> {

    private final Object mutex = new Object();
    @GuardedBy("mutex") // All read and write access to delegate must be protected by mutex.
    private final Table<Row, Column, Value> delegate = HashBasedTable.create();

    /**
     * {@link Table#put(Object, Object, Object)}
     * Associates the specified value with the specified keys. If the table
     * already contained a mapping for those keys, the old value is replaced with
     * the specified value.
     *
     * @return The old value associated with the keys or {@code null} if no previous value existed.
     */
    public Value put(Row row, Column column, Value value) {
        synchronized (mutex) {
            return delegate.put(row, column, value);
        }
    }

    /**
     * {@link java.util.concurrent.ConcurrentMap#computeIfAbsent(Object, Function)}
     *
     * Checks the existing value in the table delegate by {@link Table#get(Object, Object)} and
     * applies the given function, the function in this example should be able to handle a null input.
     *
     * @return The current value of the Table for keys, whether the function is applied or not.
     */
    public Value compute(Row row, Column column, Function<Value, Value> function) {
        synchronized (mutex) {
            Value oldValue = delegate.get(row, column);
            Value newValue = function.apply(oldValue);
            if (newValue != null) {
                delegate.put(row, column, newValue);
                return newValue;
            }
            return oldValue;
        }
    }

    /**
     * {@link Table#get(Object, Object)}
     *
     * @return The value associated with the keys or {@code null} if no value.
     */
    public Value get(Row row, Column column) {
        synchronized (mutex) {
            return delegate.get(row, column);
        }
    }

    /**
     * {@link Table#row(Object)}
     *
     * @return An immutable map view of the columns in the table.
     */
    public Map<Column, Value> get(Row row) {
        synchronized (mutex) {
            // Since we are exposing
            return ImmutableMap.copyOf(delegate.row(row));
        }
    }

    @Override
    public String toString() {
        // Even toString needs protection.
        synchronized (mutex) {
            return MoreObjects.toStringHelper(this)
                .add("delegate", delegate)
                .toString();
        }
    }
}

对于 Long 的特定行为:

/**
 * Provides support for similar behaviour as AtomicLongMap.
 *
 * Created by James on 28/02/2017.
 */
public class SynchronizedLongMultimap<Row, Column> extends SynchronizedMultimap<Row, Column, Long> {

    /**
     * @return Adds delta to the current value and returns the new value. Or delta if no previous value.
     */
    public long addAndGet(Row row, Column column, long delta) {
        return compute(row, column,
            (Long oldValue) -> (oldValue == null) ? delta : oldValue + delta);
    }

    /**
     * @return Increments the current value and returns the new value. Or 1 if no previous value.
     */
    public long increment(Row row, Column column) {
        return compute(row, column, (Long oldValue) -> (oldValue == null) ? 1 : oldValue + 1);
    }

    /**
     * @return Decrements the current value and returns the new value. Or -1 if no previous value.
     */
    public long decrement(Row row, Column column) {
        return compute(row, column, (Long oldValue) -> (oldValue == null) ? -1 : oldValue - 1);
    }
}

添加单元测试以显示逻辑

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo;

import com.google.common.collect.ImmutableMap;
import org.junit.Test;

/**
 * Test simple functionality of the Map is sound.
 *
 * Created by James on 28/02/2017.
 */
public class SynchronizedLongMultimapTest {

    private final SynchronizedLongMultimap<String, String> map = new SynchronizedLongMultimap<>();

    @Test
    public void addAndGet_SingleCell() {
        // add and get sets the initial value to the delta
        assertThat(map.addAndGet("0", "0", 1), equalTo(1L));
        assertThat(map.addAndGet("0", "0", 1), equalTo(2L));
        assertThat(map.addAndGet("0", "0", 0), equalTo(2L));
        assertThat(map.addAndGet("0", "0", -2), equalTo(0L));
    }
    @Test
    public void addAndGet_RangeCells() {
        // add and get sets the initial value to the delta
        assertThat(map.addAndGet("0", "1", 123), equalTo(123L));

        // add and get sets the initial value to the delta
        assertThat(map.addAndGet("1", "1", 42), equalTo(42L));
        // add and get adds the delta to the existing value
        assertThat(map.addAndGet("1", "1", -42), equalTo(0L));
    }

    @Test
    public void increment() {
        // increment sets the initial value to one
        assertThat(map.increment("0", "0"), equalTo(1L));
        // then adds one each time it's called
        assertThat(map.increment("0", "0"), equalTo(2L));
    }

    @Test
    public void decrement(){
        // decrement sets the initial value to -1 if no previous value
        assertThat(map.decrement("apples", "bananas"), equalTo(-1L));
        // then decrements that
        assertThat(map.decrement("apples", "bananas"), equalTo(-2L));
    }

    @Test
    public void get_PreviousValueIsNull() {
        assertThat(map.get("toast", "bananas"), equalTo(null));
        // even if we ask again
        assertThat(map.get("toast", "bananas"), equalTo(null));
    }

    @Test
    public void get_ProvidedByPut() {
        assertThat(map.put("toast", "corn flakes", 17L), equalTo(null));
        // then we get what we put in
        assertThat(map.get("toast", "corn flakes"), equalTo(17L));
    }

    @Test
    public void get_ColumnMap() {
        // Expected behaviour from MultiKeyMap question
        assertThat(map.put("a", "A", 1L), equalTo(null));
        assertThat(map.put("a", "B", 2L), equalTo(null));
        assertThat(map.put("b", "C", 3L), equalTo(null));

        // then we can get a single value
        assertThat(map.get("a", "A"), equalTo(1L));
        // or a Map
        assertThat(map.get("a"), equalTo(ImmutableMap.of("A", 1L, "B", 2L)));
        // even if that Map only has a single value
        assertThat(map.get("b"), equalTo(ImmutableMap.of("C", 3L)));
    }
}

【讨论】:

  • 感谢您的回答。只是想知道get方法Map&lt;Column, Value&gt; get(Row row) Value get(Row row, Column column)也必须同步吗?或者因为它们是读操作,它们可以是无锁的吗?
  • @Learner 是的,在此示例中它们需要同步,否则结果将是不确定的。可以用另一种方式保护它们,使用 ReadWriteLock 代替同步块。
  • +1 感谢@James,您的意思是仅将ReadWriteLock 用于使用代码中显示的同步进行获取/读取操作和写入操作?
  • 嗨@Learner,在选择并发策略时需要小心,因为混合方法可能会导致很多混乱和奇怪的结果。
【解决方案2】:

您可以将Tables.synchronizedTable(Table table)Long 值一起使用。这将为您提供 guava Table 的线程安全实现,如下所示:

Table<R, C, V> table = Tables.synchronizedTable(HashBasedTable.<R, C, V>create());
 ...
 Map<C, V> row = table.row(rowKey);  // Needn't be in synchronized block
 ...
 synchronized (table) {  // Synchronizing on table, not row!
   Iterator<Map.Entry<C, V>> i = row.entrySet().iterator(); // Must be in synchronized block
   while (i.hasNext()) {
     foo(i.next());
   }
 }

注意:不要错过用户在访问其任何集合视图时需要在返回的表上手动同步的重要建议,因为这些集合视图由 @ 返回987654325@ 方法未同步。

【讨论】:

  • 很好的答案 @kuldeep-jain 但这仅在 Guava 的快照版本中可用。
  • +1 哇,这很好,我希望它被发布。我们有 guava 22.0 版本发布的日期吗?我找不到那个。
  • 是的,这在 22.0-SNAPSHOT 版本中可用。我不确定什么时候会发布。我认为现在,您可以继续采用@James 在他的回答中建议的方法。
  • 我认为不会很远
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-03-04
  • 1970-01-01
  • 1970-01-01
  • 2011-10-09
  • 1970-01-01
  • 2013-03-20
  • 1970-01-01
相关资源
最近更新 更多