线程安全多少太多了?

时间:2009-11-30 11:50:51

标签: java multithreading concurrency thread-safety

我最近一直在阅读 Java Concurrency in Practice - 这本好书。如果你认为你知道并发是如何工作的,但是大部分时间你都面对真正的问题,感觉SWAG是你能做的最多,那么本书肯定会对这个话题有所了解。当你尝试在线程之间共享数据时,有多少东西实际上会出错,这有点可怕。我想这让我对线程安全感到有点疯狂。现在我担心的是,由于同步太多,我可能会遇到一些活动问题。这是一段代码来说明:

   private final Hashtable<String, AtomicInteger> userSessions =
new Hashtable<String, AtomicInteger>();

   public void registerUser(String userLogin) {
       synchronized(userSessions) {
           AtomicInteger sessionCount = userSessions.get(userLogin);
           if (sessionCount != null) {
               sessionCount.incrementAndGet();
           } else {
               userSessions.put(userLogin, new AtomicInteger(1));
           }
       }
   }

   public void unregisterUser(String userLogin) {
       synchronized(userSessions) {
           AtomicInteger sessionCount = userSessions.get(userLogin);
           if (sessionCount != null) {
               sessionCount.decrementAndGet();
           }
       }
   }

   public boolean isUserRegistered(String userLogin) {
       synchronized(userSessions) {
           AtomicInteger sessionCount = userSessions.get(userLogin);
           if (sessionCount == null) {
               return false;
           }
           return sessionCount.intValue() > 0;
       }
   }

我试着把它弄好:同步集合在静态部分构造并存储在静态最终引用中以便安全发布,锁定集合(而不是this - 这样我就不会阻止整个类代码存在于)并使用原始包装类进行基元。这本书提到过度使用这也可能会引起问题,但似乎我需要更多的时间来完全包裹它。 您如何使此代码具有线程安全性,并确保它不会受到活动和性能问题的影响?

编辑:把它变成实例方法和变量,原来一切都被声明为静态 - 糟糕,糟糕的设计。还使userSessions变为私有(不知何故,我之前将其公开)。

5 个答案:

答案 0 :(得分:14)

使用ConcurrentHashMap以便您可以使用putIfAbsent。您无需AtomicInteger代码进行同步。

   public final ConcurrentMap<String, AtomicInteger> userSessions =
       new ConcurrentHashMap<String, AtomicInteger>();

   public void registerUser(String userLogin) {
       AtomicInteger newCount = new AtomicInteger(1);
       AtomicInteger oldCount = userSessions.putIfAbsent(userLogin, newCount);
       if (oldCount != null) {
           oldCount.incrementAndGet();
       }
   }

   public void unregisterUser(String userLogin) {
       AtomicInteger sessionCount = userSessions.get(userLogin);
       if (sessionCount != null) {
           sessionCount.decrementAndGet();
       }
   }

   public boolean isUserRegistered(String userLogin) {
       AtomicInteger sessionCount = userSessions.get(userLogin);
       return sessionCount != null && sessionCount.intValue() > 0;
   }

注意,这个泄漏......

尝试非泄漏版本:

   public final ConcurrentMap<String, Integer> userSessions =
       new ConcurrentHashMap<String, Integer>();

   public void registerUser(String userLogin) {
       for (;;) {
           Integer old = userSessions.get(userLogin);
           if (userSessions.replace(userLogin, old, old==null ? 1 : (old+1)) {
                break;
           }
       }
   }
   public void unregisterUser(String userLogin) {
       for (;;) {
           Integer old = userSessions.get(userLogin);
           if (old == null) {
               // Wasn't registered - nothing to do.
               break;
           } else if (old == 1) {
               // Last one - attempt removal.
               if (userSessions.remove(userLogin, old)) {
                   break;
               }
           } else {
               // Many - attempt decrement.
               if (userSessions.replace(userLogin, old, old-1) {
                   break;
               } 
           }
       }
   }
   public boolean isUserRegistered(String userLogin) {serLogin);
       return userSessions.containsKey(userLogin);
   }

答案 1 :(得分:7)

首先:不要使用Hashtable!它很旧,而且非常慢 附加:如果您已经在更高级别进行同步,则不需要在较低级别进行同步(对于AtomicInteger-thing也是如此)。

根据这里需要的用例,我在这里看到了不同的方法。

读/写方法

假设您经常调用isUserRegistered方法而其他方法只是偶然调用,一个好方法是读写锁:允许同时有多个读取,但只允许一个写锁定来统治它们(只有在没有获得其他锁定的情况下才能获得)。

private static final Map<String, Integer> _userSessions =
  new HashMap<String, Integer>();

private ReadWriteLock rwLock =
  new ReentrantReadWriteLock(false); //true for fair locks

public static void registerUser(String userLogin) {
  Lock write = rwLock.writeLock();
  write.lock();
  try {
       Integer sessionCount = _userSessions.get(userLogin);
       if (sessionCount != null) {
           sessionCount = Integer.valueOf(sessionCount.inValue()+1);
       } else {
           sessionCount = Integer.valueOf(1)
       }
       _userSessions.put(userLogin, sessionCount);
   } finally {
     write.unlock();
   }
}

public static void unregisterUser(String userLogin) {
  Lock write = rwLock.writeLock();
  write.lock();
  try {
       Integer sessionCount = _userSessions.get(userLogin);
       if (sessionCount != null) {
           sessionCount = Integer.valueOf(sessionCount.inValue()-1);
       } else {
           sessionCount = Integer.valueOf(0)
       }
       _userSessions.put(userLogin, sessionCount);
   } finally {
     write.unlock();
   }
}

public static boolean isUserRegistered(String userLogin) {
  boolean result;

  Lock read = rwLock.readLock();
  read.lock();
  try {
       Integer sessionCount = _userSessions.get(userLogin);
       if (sessionCount != null) {
           result = sessionCount.intValue()>0
       } else {
           result = false;
       }
   } finally {
     read.unlock();
   }

   return false;
}

亲:简单易懂 Con:如果经常调用write方法,则无法扩展

小原子操作方法

想法是做小步骤,这些步骤都是原子的。 无论如何,这将带来非常好的表现,但这里有许多隐藏的陷阱。

public final ConcurrentMap<String, AtomicInteger> userSessions =
   new ConcurrentHashMap<String, AtomicInteger>();
//There are other concurrent Maps for different use cases

public void registerUser(String userLogin) {
  AtomicInteger count;
  if (!userSession.containsKey(userLogin)){
    AtomicInteger newCount = new AtomicInteger(0);
    count = userSessions.putIfAbsent(userLogin, newCount);
    if (count == null){
      count=newCount;
    }
    //We need ifAbsent here, because another thread could have added it in the meantime
  } else {
    count = userSessions.get(userLogin);
  }
  count.incrementAndGet();
}

public void unregisterUser(String userLogin) {
  AtomicInteger sessionCount = userSessions.get(userLogin);
  if (sessionCount != null) {
    sessionCount.decrementAndGet();
  }
}

public boolean isUserRegistered(String userLogin) {
  AtomicInteger sessionCount = userSessions.get(userLogin);
  return sessionCount != null && sessionCount.intValue() > 0;
}

专业:非常好 骗局:不直观,很快就会很复杂,并不总是可能,很多隐藏的陷阱

每用户锁定方法

这将为不同的用户创建锁,假设有很多不同的用户。您可以使用一些小的原子操作创建锁或监视器,并锁定它们而不是完整列表 对于这个小例子来说这可能有点过头了,但对于非常复杂的结构来说,这可能是一个优雅的解决方案。

答案 2 :(得分:2)

从代码中看,_userSessions上的同步应该足够了,因为您没有公开AtomicInteger个对象。

在这种情况下,不需要AtomicInteger提供的额外安全性,因此实质上您在此处将其用作可变Integer。如果您担心AtomicInteger中的额外开销(或者有点丑陋:将int [1]添加到地图中),您可以将包含会话计数的嵌套静态类作为地图中的唯一属性放置他们没有暴露在这门课外。)

答案 3 :(得分:2)

好书,我最近自己读了。

在上面的代码中,我唯一的注意事项是在synchronized块中不需要AtomicInteger,但我怀疑性能是否会引人注意。

跟踪性能的最佳方法是测试它。围绕框架的关键区域设置自动集成负载测试并跟踪性能。负载测试,如果它包含足够宽的时间窗口和丰富的工作流程,也可能会捕获您创建的任何死锁。

虽然死锁似乎很容易避免,但它们很容易以相当简单的工作流模式出现。

A类锁定资源然后调用B(可能像get / set一样简单),它也会锁定资源。 另一个线程调用B锁定资源,然后调用A导致死锁。

使用富框架时,映射工作流以查看类如何交互是很有用的。您可能能够发现此类问题。然而,对于非常大的框架,它们可以顺便说一句。我发现的最好的防御是将锁定隔离到可能的最小区域,并且在同步块内非常清楚类外的调用。同时创建大量的负载测试帮助。

答案 4 :(得分:1)

我偶然发现了这个问题,寻找有关如何制作人们称之为“并发计数地图”的建议 - 特别针对ConcurrentHashMapAtomicInteger的使用进行搜索。

以下是the highest-rated answer的修改版本,使用AtomicInteger并且不会泄露。在我的(有限)测试中,这似乎比Integer版本快得多。我还要注意,在ConcurrentMap.get()之前使用ConcurrentMap.putIfAbsent()似乎可以节省大量时间。

private final ConcurrentMap<String, AtomicInteger> userSessions =
   new ConcurrentHashMap<String, AtomicInteger>();

public void registerUser(String userLogin) {
    AtomicInteger oldCount = userSessions.get(key);
    if(oldCount!=null && getAndIncrementIfNonZero(oldCount)>0) return;
    AtomicInteger newCount = new AtomicInteger(1);
    while(true) {
        oldCount = userSessions.putIfAbsent(key, newCount);
        if(oldCount==null) return;
        if(getAndIncrementIfNonZero(oldCount)>0) return;
    }
}

public void unregisterUser(String userLogin) {
   AtomicInteger sessionCount = userSessions.get(userLogin);
   if (sessionCount != null) {
       int endingCount = sessionCount.decrementAndGet();
       if(endingCount==0) userSessions.remove(userLogin);
   }
}

public boolean isUserRegistered(String userLogin) {
   AtomicInteger sessionCount = userSessions.get(userLogin);
   return sessionCount != null && sessionCount.intValue() > 0;
}

private static int getAndIncrementIfNonZero(AtomicInteger ai) {
    while(true) {
        int current = ai.get();
        if(current==0) return 0;
        if(ai.compareAndSet(current, current+1)) return current;
    }
}

速度可能与原始海报的问题不太相关,但这种“计数图”的其他应用可能会从效率中受益。