【问题标题】:HashMap synchronization for Map with value-incrementation使用值递增的 Map 的 HashMap 同步
【发布时间】:2012-03-21 00:57:29
【问题描述】:

我有一个关于 HashMap 同步的问题。背景是我正在尝试实现一种简单的蛮力检测方法。我将使用一个以用户名作为键的映射,用于保存用户的失败登录尝试次数。如果登录失败,我想做这样的事情:

    Integer failedAmount = myMap.get("username");
    if (failedAmount == null) {
        myMap.put("username", 1);
    } else {
        failedAmount++;
        if (failedAmount >= THRESHOLD) {
            // possible brute force detected! alert admin / slow down login
            // / or whatever
        }
        myMap.put("username", failedAmount);
    }

我现在想到的机制非常简单:我会跟踪一整天,然后在午夜或类似的时候 clear() HashMap。

所以我的问题是: 我可以为此使用的最好/最快的地图实现是什么?我需要一个完全同步的 Map (Collections.sychronizedMap()) 还是一个 ConcurrentHashMap 就足够了?或者甚至只是一个普通的 HashMap?我想如果一些增量滑过,这不是什么大问题?

【问题讨论】:

    标签: java synchronization synchronized


    【解决方案1】:

    我会使用 ConcurrentHashMapAtomicInteger http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/atomic/AtomicInteger.html 的组合。

    使用AtomicInteger 不会帮助您进行比较,但它会帮助您保持数字准确 - 无需分两步执行 ++ 和 put。

    ConcurrentHashMap 上,我将使用putIfAbsent 方法,这将消除您的第一个if 条件。

    AtomicInteger failedAmount = new AtomicInteger(0);
    
    failedAmount = myMap.putIfAbsent("username", failedAmount);
    
    if (failedAmount.incrementAndGet() >= THRESHOLD) {
        // possible brute force detected! alert admin / slow down login
        // / or whatever
    }
    

    【讨论】:

    • 你仍然需要一个同步块。
    • 糟糕。没有看到 putIfAbsent :) 非常优雅的解决方案 :) +1!
    • 这真的很有帮助!不知道 AtomicInteger,让“增量”和“获取”一个原子操作真是个好主意。谢谢!
    • 如果 myMap 不包含“用户名”的映射,那么你会得到 NullPointerException
    【解决方案2】:

    除非您同步执行更新的整个代码块,否则无论如何它都不会像您期望的那样工作。

    同步地图只是确保如果您同时多次调用put 时不会发生任何令人讨厌的事情。它不能确定

    myMap.put("username", myMap.get("username") + 1);
    

    以原子方式执行。

    您应该真正同步执行更新的整个块。通过使用一些Semaphore 或使用synchronized 关键字。例如:

    final Object lock = new Object();
    
    ...
    
    synchronized(lock) {
    
        if (!myMap.containsKey(username))
            myMap.put(username, 0);
    
        myMap.put(username, myMap.get(username) + 1);
    
        if (myMap.get(username) >= THRESHOLD) {
            // possible brute force detected! alert admin / slow down login
        }
    }
    

    【讨论】:

      【解决方案3】:

      我看到的最好方法是将失败计数器与用户对象一起存储,而不是存储在某种全局映射中。这样就不会出现同步问题。

      如果您仍然想使用地图,如果您使用可变计数器对象,则可以使用部分同步的方法:

      static class FailCount {
          public int count;
      }
      
      // increment counter for user
      FailCount count;
      synchronized (lock) {
          count = theMap.get(user);
          if (count == null) {
              count = new FailCount();
              theMap.put(user, count);
          }
      }
      count.count++;
      

      但很可能这里的任何优化尝试都是浪费时间。它不像你的系统会在一秒钟内处理数百万次登录失败,所以你的原始代码应该就可以了。

      【讨论】:

      • +1 您可以将 FailCount 设为 AtomicInteger 以使其线程安全。
      【解决方案4】:

      我在这里看到的最简单的解决方案是将此代码提取到一个单独的函数中并同步。 (或将所有代码放入同步块中)。所有其他保持不变。地图变量应该是最终的。

      【讨论】:

        【解决方案5】:

        仅当您的监控应用程序是多线程的时,才需要使用同步的HashMapConcurrentHashMap。如果是这种情况,ConcurrentHashMap 在高负载/争用情况下的性能要好得多。

        即使存在 一个 writer/updater 线程,我也不敢使用具有多个线程的非同步结构。这不仅仅是丢失一些增量的问题 - HashMap 本身的内部结构可能已损坏。

        也就是说,如果你想确保没有增量丢失,那么即使是同步的Map 也是不够的:

        • UserX 尝试登录
        • 线程 A 获得“UserX”的计数 N
        • UserX 尝试再次登录
        • 线程 B 获得“UserX”的计数 N
        • A 将 N + 1 放到地图上
        • B 将 N + 1 放到地图上
        • 地图现在包含 N + 1 而不是 N + 2

        为避免这种情况,请在整个 get/set 操作中使用同步块,或者在计数器中使用类似于 AtomicInterer 而不是普通的 Integer 的内容。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2014-01-03
          • 1970-01-01
          • 2015-09-25
          • 1970-01-01
          • 2017-05-18
          • 1970-01-01
          • 2010-11-20
          相关资源
          最近更新 更多