锁定特定对象的Java线程

我有一个Web应用程序,我正在使用Oracle数据库,我有一个基本上像这样的方法:

public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) { if (!methodThatChecksThatObjectAlreadyExists) { storemyObject() //pseudo code } // Have to do a lot other saving stuff, because it either saves everything or nothing commit() // pseudo code to actually commit all my changes to the database. } 

现在没有任何类型的同步,所以n个线程当然可以自由地访问这个方法,当2个线程进入这个方法同时检查时会出现问题,当然还没有任何东西,然后他们都可以提交事务,创建重复的对象。

我不想在我的数据库中使用唯一的密钥标识符来解决这个问题,因为我认为我不应该捕获那个SQLException

我也无法在提交之前检查,因为有几个检查不仅1 ,这将花费相当多的时间。

我对锁和线程的体验是有限的,但我的想法基本上是将这个代码锁定在它接收的对象上。 我不知道例如说我收到一个整数对象,并且我用值1锁定我的整数,这只会阻止具有值为1的另一个Integer的线程进入,而所有其他具有value != 1线程都可以自由进入?,这是怎么回事?

此外,如果这是它的工作原理,锁对象如何比较? 它是如何确定它们实际上是同一个对象的? 关于这一点的好文章也将受到赞赏。

你怎么解决这个问题?

你的想法很好。 这是简单/天真的版本,但不太可行:

 public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) { synchronized (theObjectIwantToSave) { if (!methodThatChecksThatObjectAlreadyExists) { storemyObject() //pseudo code } // Have to do a lot other saving stuff, because it either saves everything or nothing commit() // pseudo code to actually commit all my changes to the database. } } 

此代码使用对象本身作为锁。 但它必须是同一个对象(即objectInThreadA == objectInThreadB)才能工作。 如果两个线程在一个对象的副本上运行 – 例如具有相同的“id”,那么你需要同步整个方法:

  public static synchronized void saveSomethingImportantToDataBase(Object theObjectIwantToSave) ... 

这当然会大大降低并发性(吞吐量将使用该方法一次降至一个线程 – 要避免)。

或者找到一种基于保存对象获取相同锁对象的方法,如下所示:

 private static final ConcurrentHashMap LOCKS = new ConcurrentHashMap(); public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) { synchronized (LOCKS.putIfAbsent(theObjectIwantToSave.getId(), new Object())) { .... } LOCKS.remove(theObjectIwantToSave.getId()); // Clean up lock object to stop memory leak } 

推荐的最后一个版本:它将确保共享相同“id”的两个保存对象使用相同的锁对象锁定 – 方法ConcurrentHashMap.putIfAbsent()是线程安全的,因此“这将起作用”,它需要只有objectInThreadA.getId().equals(objectInThreadB.getId())才能正常工作。 此外,getId()的数据类型可以是任何东西,包括由于java的自动装箱而产生的原语(例如int )。

如果为对象重写equals()hashcode() ,那么你可以使用对象本身而不是object.getId() ,这将是一个改进(感谢@TheCapn指出这一点)

此解决方案仅适用于一个JVM。 如果您的服务器是集群的,那么整个不同的球类游戏和java的锁定机制将无法帮助您。 您将不得不使用群集锁定解决方案,这超出了本答案的范围。

这是一个改编自And360对Bohemian答案的评论的选项,它试图避免竞争条件等。虽然我更喜欢我对这个问题的另一个答案 ,但是:

 import java.util.HashMap; import java.util.concurrent.atomic.AtomicInteger; // it is no advantage of using ConcurrentHashMap, since we synchronize access to it // (we need to in order to "get" the lock and increment/decrement it safely) // AtomicInteger is just a mutable int value holder // we don't actually need it to be atomic static final HashMap locks = new HashMap(); public static void saveSomethingImportantToDataBase(Object objectToSave) { AtomicInteger lock; synchronized (locks) { lock = locks.get(objectToSave.getId()); if (lock == null) { lock = new AtomicInteger(1); locks.put(objectToSave.getId(), lock); } else lock.incrementAndGet(); } try { synchronized (lock) { // do synchronized work here (synchronized by objectToSave's id) } } finally { synchronized (locks) { lock.decrementAndGet(); if (lock.get() == 0) locks.remove(id); } } } 

您可以将这些拆分为帮助方法“获取锁定对象”和“释放锁定”,或者也可以将其拆分为清除代码。 这种方式比我的其他答案感觉更加笨拙。

我的意见是你没有遇到真正的线程问题。

你最好让DBMS自动分配一个非冲突的行id。

如果需要使用现有的行ID,则将它们存储为线程局部变量。 如果不需要共享数据,则不在线程之间共享数据。

http://download.oracle.com/javase/6/docs/api/java/lang/ThreadLocal.html

在应用程序服务器或Web容器中,Oracle dbms在保持数据一致方面要好得多。

“插入行时,许多数据库系统会自动生成唯一的键字段.Oracle数据库在序列和触发器的帮助下提供相同的function.JDBC 3.0引入了自动生成的键function的检索,使您可以检索此类生成的值。在JDBC 3.0中,增强了以下接口以支持检索自动生成的密钥function….“

http://download.oracle.com/docs/cd/B19306_01/java.102/b14355/jdbcvers.htm#CHDEGDHJ

如果您可以偶尔进行过度同步(即在不需要时按顺序完成工作),请尝试以下方法:

  1. 创建一个包含锁定对象的表。 越大的表,过度同步的机会越少。
  2. 对您的id应用一些哈希函数来计算表索引。 如果你的id是数字,你可以使用余数(模数)函数,如果它是一个String,使用hashCode()和余数。
  3. 从表中获取锁定并同步它。

IdLock类:

 public class IdLock { private Object[] locks = new Object[10000]; public IdLock() { for (int i = 0; i < locks.length; i++) { locks[i] = new Object(); } } public Object getLock(int id) { int index = id % locks.length; return locks[index]; } 

}

及其用途:

 private idLock = new IdLock(); public void saveSomethingImportantToDataBase(Object theObjectIwantToSave) { synchronized (idLock.getLock(theObjectIwantToSave.getId())) { // synchronized work here } } 

波希米亚的答案似乎有竞争条件问题,如果一个线程在同步部分,而另一个线程从地图中删除同步对象,等等。所以这是一个利用WeakRef的替代方案。

 // there is no synchronized weak hash map, apparently // and Collections.synchronizedMap has no putIfAbsent method, so we use synchronized(locks) down below WeakHashMap locks = new WeakHashMap<>(); public void saveSomethingImportantToDataBase(DatabaseObject objectToSave) { Integer lock; synchronized (locks) { lock = locks.get(objectToSave.getId()); if (lock == null) { lock = new Integer(objectToSave.getId()); locks.put(lock, lock); } } synchronized (lock) { // synchronized work here (synchronized by objectToSave's id) } // no releasing needed, weakref does that for us, we're done! } 

以及如何使用上述样式系统的更具体的示例:

 static WeakHashMap locks = new WeakHashMap<>(); static Object getSyncObjectForId(int id) { synchronized (locks) { Integer lock = locks.get(id); if (lock == null) { lock = new Integer(id); locks.put(lock, lock); } return lock; } } 

然后在其他地方使用它:

 ... synchronized (getSyncObjectForId(id)) { // synchronized work here } ... 

这样做的原因基本上是,如果两个具有匹配键的对象进入临界块,则第二个将检索第一个已经使用的锁(或者留下的那个尚未进行GC的锁)。 但是,如果它未被使用,则两者都将保留该方法并删除它们对锁对象的引用,因此可以安全地收集它。

如果你想要使用有限的“已知大小”的同步点(最终不必减小大小),你可以避免使用HashMap并使用ConcurrentHashMap,而使用它的putIfAbsent方法可能是更容易理解。

 public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) { synchronized (theObjectIwantToSave) { if (!methodThatChecksThatObjectAlreadyExists) { storemyObject() //pseudo code } // Have to do a lot other saving stuff, because it either saves everything or nothing commit() // pseudo code to actually commit all my changes to the database. } } 

synchronized关键字锁定您想要的对象,以便其他方法无法访问它。

我认为你没有别的选择,只能选择一个你似乎不想做的解决方案。

在您的情况下,我认为objectYouWantToSave上的任何类型的同步都不会起作用,因为它们基于Web请求。 因此,每个请求(在其自己的线程上)很可能会拥有它自己的对象实例。 尽管它们可能被视为逻辑上相等,但这对于同步并不重要。

synchronized关键字(或其他同步操作)必须但不足以解决您的问题。 您应该使用数据结构来存储使用的整数值。 在我们的示例中使用了HashSet。 不要忘记从hashset中删除太旧的记录。

 private static HashSet isUsed= new HashSet (); public synchronized static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) { if(isUsed.contains(theObjectIwantToSave.your_integer_value) != null) { if (!methodThatChecksThatObjectAlreadyExists) { storemyObject() //pseudo code } // Have to do a lot other saving stuff, because it either saves everything or nothing commit() // pseudo code to actually commit all my changes to the database. isUsed.add(theObjectIwantToSave.your_integer_value); } } 

要回答有关锁定整数的问题,简短的答案是否定的 – 它不会阻止具有相同值的另一个Integer实例的线程进入。 答案很长:取决于你如何获得Integer – by构造函数,重用一些实例或者valueOf(使用一些缓存)。 无论如何,我不会依赖它。

一个可行的工作解决方案是使方法同步:

 public static synchronized void saveSomethingImportantToDataBase(Object theObjectIwantToSave) { if (!methodThatChecksThatObjectAlreadyExists) { storemyObject() //pseudo code } // Have to do a lot other saving stuff, because it either saves everything or nothing commit() // pseudo code to actually commit all my changes to the database. } 

这可能不是性能方面的最佳解决方案,但在您找到更好的解决方案之前,它可以保证工作(请注意,如果您不在群集环境中)。