我们如何在HashMap中进行入门级锁定?

Moh*_*vaf 1 java concurrency hashmap

因为,statckoverflow不允许在原始问题中为你的问题添加更多东西(你只能添加注释,而不是代码)我在这里问一个顺序问题我的原始问题: 我们可以为每个条目而不是ConcurrentHashMap使用Synchronized吗?

问题很简单,我不知道为什么这么简单的问题可能很多人在我之前遇到过这个问题我应该花这么多时间:/

问题是:我有一个hashmap,我想当一个线程正在处理hashMap的一个条目时,没有任何其他线程访问该对象,我不想锁定整个hashMap.

我知道java提供了ConcurrentHashMap,但是当你想做一些比简单的put和get更复杂的事情时,ConcurrentHashMap并没有解决问题.即使是新增的函数(在Java 8中),如合并也不足以应对复杂的场景.

例如:

假设我想要一个将字符串映射到ArrayLists的哈希映射.然后例如假设我想这样做:对于密钥k,如果有任何条目,则将newString添加到其ArrayList,但如果没有k的条目,则为k创建条目,使其ArrayList具有newString.

我以为我可以这样做:

                ArrayList<String> tm =new ArrayList<String>();
                tm.add(newString);
                Object result = map.putIfAbsent(k, tm);
                if  (result != null)
                {
                    map.get(k).add(newString);
                }
Run Code Online (Sandbox Code Playgroud)

但它不起作用,为什么?假设putIfAbset返回null以外的其他内容,则表示map已经有一个带有键k的条目,因此我将尝试将newString添加到已存在条目的ArrayList中,但是在添加之前,另一个线程可能会删除该条目,并且然后我会得到NullPointerException!

所以,我发现很难正确地编写这样的东西.

但我在想,如果我能简单地锁定那个条目,那么生活将是美好的!

在我之前的帖子中,我提出了一些非常简单的事实,实际上消除了对concurrentHashMap的需求,并提供了入门级锁定,但有些人认为这不是真的,因为Long不是不可变的......我没有把它弄好.

现在,我实现并测试了它,它看起来不错,但我不知道为什么其他更有经验的开发人员告诉我它不是线程安全的:(

这是我测试的确切代码:

MainThread:

import java.util.HashMap;

public class mainThread {

public static HashMap<String, Long> map = new HashMap<String, Long>();

public static void main (String args[])
{
    map.put("k1", new Long(32));


    synchronized(map.get("k1"))
    {
        Thread t = new Thread(new threadA());
        t.start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
}
}
Run Code Online (Sandbox Code Playgroud)

ThreadA中:

public class ThreadA implements Runnable {

    @Override
    public void run() {
    mainThread.map.put("k2", new Long(21));
    System.out.println(mainThread.map.get("k2"));


    synchronized (mainThread.map.get("k1")) {
        System.out.println("Insdie synchronized of threadA");
    }
}
}
Run Code Online (Sandbox Code Playgroud)

它工作正常!它打印21,并且在5秒后,mainThread释放了map.get("k1")的锁,它打印出"Insdie synchronized of threadA"

那么,为什么使用这种简单的方法我们无法提供入门级锁定?!为什么并发应该是那么复杂的Lol(开玩笑)

Ste*_*n C 5

首先,我所知道的没有提供入门级锁定的标准地图实现.

但我认为你可以避免这种需要.例如

更新...纠正错误

ArrayList<String> tm = new ArrayList<String>();
ArrayList<String> old = map.putIfAbsent(k, tm);
if (old != null) {
    tm = old;
}
synchronized (tm) {
    // can now add / remove entries and this will appear as an atomic
    // actions to other threads that are using `synchronized` to 
    // access or update the list
    tm.add(string1);
    tm.add(string2);
}
Run Code Online (Sandbox Code Playgroud)

是的,另一个线程可能会更新此线程(可能)插入它之间的hashmap条目中的列表,并且此线程将其锁定.但是,这没关系.(更正)putIfAbsent和后面的测试确保每个人都使用并锁定相同的列表.

(假设:插入/更新条目时,所有线程都使用此逻辑.)


如果列表变空是以原子方式删除列表很困难,但我认为通常不需要这样做.


更新2

有一个更好的方法:

ArrayList<String> tm = map.computeIfAbsent(k, ArrayList::new);
synchronized (tm) {
    ...
}
Run Code Online (Sandbox Code Playgroud)

(谢谢斯图尔特)


更新3

我们也可以通过合并来做到这一点.

也许是吧.像这样的东西:

ArrayList<String> tm = new ArrayList<String>;
tm.add(...);
...
map.merge(key, tm, (oldV, newV) -> {oldV.addAll(newV); return oldV});
Run Code Online (Sandbox Code Playgroud)

缺点是你要对所有元素进行双重处理tm; 即添加到2个单独的列表(其中一个列出你的方式).

但你也可以这样做:

map.merge(key, tm, (oldV, newV) -> {
      oldV.removeAll(newV); 
      return oldV.size() == 0 ? null : oldV}
);
Run Code Online (Sandbox Code Playgroud)

我担心的是,javadoc没有明确说明在oldV发生这种情况时该值将被锁定.它说:

"整个方法调用是以原子方式执行的.在计算正在进行时,其他线程可能会阻止某些在此映射上尝试的更新操作......"

......但它没有明确说明在发生这种情况时价值存在互斥.(例如,将此方法与putIfAbsent/ computeIfAbsent和显式synchronized块混合很可能是危险的.锁定很可能是在不同的对象上.)