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

时间:2016-07-21 02:46:33

标签: java concurrency hashmap

因为,statckoverflow不允许在原始问题中为你的问题添加更多内容(你只能添加注释,而不是代码)我在这里问一个顺序问题我原来的问题: Can we use Synchronized for each entry instead of ConcurrentHashMap?

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

问题是:我有一个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);
                }

但它不起作用,为什么?假设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();
        }

    }
}
}

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");
    }
}
}

工作正常!它打印21,并且在5秒之后,mainThread释放了map.get的锁定(&#34; k1&#34;),它打印&#34; Insdie同步的threadA&#34;

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

3 个答案:

答案 0 :(得分: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);
}

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

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

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

更新2

有一种更好的方法:

ArrayList<String> tm = map.computeIfAbsent(k, ArrayList::new);
synchronized (tm) {
    ...
}

(谢谢斯图尔特)

更新3

  

我们也可以通过合并来实现。

也许,是的。像这样:

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

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

但你也可以这样做:

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

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

  

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

...但是当发生这种情况时,它没有明确说明上存在互斥。 (例如,将此方法与putIfAbsent / computeIfAbsent和明确的synchronized块混合使用很可能会造成危险。锁定最有可能出现在不同的对象上。)

答案 1 :(得分:0)

嗯,第一个巨大的问题是你甚至没有尝试对put个电话进行任何锁定。对于常规HashMap,这些不是自动线程安全的。您似乎认为单独的HashMap条目是自动完全独立的,但HashMaps不会那样工作。

即使您修复了put问题(可能还需要ConcurrentHashMap或整个地图锁),实际锁定的部分也不会安全锁定。

假设线程1 put是条目"k1": 1,线程2尝试get("k1")。线程2会看到什么?

好吧,在get调用已经完成之前,线程2甚至都没有尝试获取任何锁定。 get电话完全没有受到保护!如果putget之间没有任何先发生的关系,则get调用可能看不到该条目,或者它可能会看到该条目,或者它可能会看到不一致的地图中间状态和可怕的崩溃。

同步get来电的结果太晚了。

答案 2 :(得分:0)

我想我终于找到了使用合并功能的解决方案。我提供了一个示例,我将编辑此帖子以便其他人阅读,但我现在发帖以获得您的反馈。

以下是ConcurrentHashMap的示例,其中ConcurrentHashMaps作为其值(为了示例,23和1只是两个随机值):

function addPKCS5Padding($input)
{
     $blockSize = 16;
     $padd = "";
     $length = $blockSize - (strlen($input) % $blockSize);
     for ($i = 1; $i <= $length; $i++)
{
     $padd .= chr($length);
}
     return $input . $padd;
}

function removePKCS5Padding($input)
{
    $blockSize = 16;
    $padChar = ord($input[strlen($input) - 1]);
    $unpadded = substr($input, 0, (-1) * $padChar);
    return $unpadded;
}


function encryptAes($string, $key)
{
    $string = addPKCS5Padding($string);
    $crypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_CBC, $key);
    return  strtoupper(bin2hex($crypt));
}


function decryptAes($strIn, $myEncryptionPassword)
{

#Sagepay specific - remove the '@'
$strIn = substr($strIn,1);

    $strInitVector = $myEncryptionPassword;
    $strIn = pack('H*', $hex);
    $string = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $myEncryptionPassword, $strIn, MCRYPT_MODE_CBC,$strInitVector);
    return removePKCS5Padding($string);
}

这就是代码的作用:

  1. 初始化:首先创建地图并将另一张地图放入其中,键23的值为 Long intialValue = new Long(3); Long addedValue = new Long(10); Long removingValue = new Long (5); ConcurrentHashMap<Integer, ConcurrentHashMap<Integer, Long>> map = new ConcurrentHashMap<>(); //Initialization.... ConcurrentHashMap<Integer, Long> i = new ConcurrentHashMap<Integer, Long>(); i.put(1, intialValue); map.put(23, i); //...... //addition ConcurrentHashMap<Integer, Long> c = new ConcurrentHashMap<Integer, Long>(); c.put(1, addedValue); map.merge(23, c, (oldHashMap, newHashMap) -> { oldHashMap.merge (1, c.get(1), (oldV, newV) -> { if (oldV < newV) return newV; else return oldV; }); return oldHashMap; }); //removal // we want to remove entry 1 from the inner HashMap if its value is less than 2, and if the entry is empty remove the entry from the outer HashMap ConcurrentHashMap<Integer, Long> r = new ConcurrentHashMap<Integer, Long>(); r.put(1, removingValue); map.merge (23, r, (oldHashMap, newHashMap) -> { oldHashMap.merge(1, newHashMap.get(1), (oldV, newV) -> {if (oldV < newV) return newV; else return oldV;}); return oldHashMap; }); map.remove(23, r); if (map.containsKey(23)) { System.out.println("Map contains key 23"); if (map.get(23).containsKey(1)) { System.out.println("The value for <23,1> is " + map.get(23).get(1)); } } ,用于键1。
  2. 添加:然后检查,1)如果对于键23,没有值,则为键1放置一个值为addedValue的映射,否则2)如果键23已经有值,如果值的值小于initialValue,它会检查它的值,它会用addedValue覆盖它,否则它将不管它。
  3. 删除:最后,它会检查是否为23键,以及23中值的键1,该值小于addedValue,它会删除该值,如果是删除后,键23的hashMap为空,它从主映射中删除键23。
  4. 我测试了这段代码。例如:

    • 对于3,10,5,&lt; 23,1&gt;的最终值。是10.
    • 表示20,10,11,最终值为20。
    • 对于3,10,11,最终值是什么,因为条目23是 除去。

    我希望它是线程安全的,因为我刚使用了merge方法。这段代码的一个缺点是我正在添加一些东西来映射然后删除它,因为ConcurrentHashMap没有类似于merge的删除方法。我希望我有这个方法:

    map.remove(keyToRemove,条件