锁定特定对象的Java线程

时间:2011-07-07 19:41:30

标签: java multithreading synchronization locking

我有一个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,这将需要相当长的时间。

我对锁和线程的经验是有限的,但我的想法基本上是将这个代码锁定在它接收的对象上。我不知道例如说我收到一个Integer对象,并且我用值1锁定我的Integer,这只会阻止具有值为1的另一个Integer的线程进入,以及所有其他带有value != 1的线程可以自由进入吗?这是它的工作原理吗?。

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

你会如何解决这个问题?。

10 个答案:

答案 0 :(得分:5)

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

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<Object, Object> LOCKS = new ConcurrentHashMap<Object, Object>();
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()的数据类型可以是任何东西,包括原语(例如int),因为java autoboxing

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

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

答案 1 :(得分:3)

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

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<Object, AtomicInteger> locks = new HashMap<Integer, AtomicInteger>();

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

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

答案 2 :(得分:2)

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

// there is no synchronized weak hash map, apparently
// and Collections.synchronizedMap has no putIfAbsent method, so we use synchronized(locks) down below

WeakHashMap<Integer, Integer> 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<Integer, Integer> 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&#39 ; ed)。但是,如果它未被使用,则两者都将保留该方法并删除它们对锁对象的引用,因此可以安全地收集它。

如果你有一个有限的&#34;已知的尺寸&#34;您想要使用的同步点(最终不必减小大小),您可以避免使用HashMap并使用ConcurrentHashMap,而使用putIfAbsent方法可能更容易理解。

答案 3 :(得分:1)

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

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

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

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

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

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

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

答案 4 :(得分:1)

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

  1. 创建一个包含锁定对象的表。越大的表,过度同步的可能性就越小。
  2. 将一些哈希函数应用于您的id以计算表索引。如果你的id是数字,你可以使用余数(模数)函数,如果它是一个String,使用hashCode()和余数。
  3. 从表中获取锁定并同步它。
  4. 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
      }
    }
    

答案 5 :(得分:0)

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关键字会锁定您想要的对象,以便其他任何方法都无法访问它。

答案 6 :(得分:0)

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

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

答案 7 :(得分:0)

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

private static HashSet <Integer>isUsed= new HashSet <Integer>();

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

  }
}

答案 8 :(得分:0)

要回答有关锁定整数的问题,简短答案为否 - 它不会阻止具有相同值的另一个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.
}

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

答案 9 :(得分:0)

private static final Set<Object> lockedObjects = new HashSet<>();

private void lockObject(Object dbObject) throws InterruptedException {
    synchronized (lockedObjects) {
        while (!lockedObjects.add(dbObject)) {
            lockedObjects.wait();
        }
    }
}

private void unlockObject(Object dbObject) {
    synchronized (lockedObjects) {
        lockedObjects.remove(dbObject);
        lockedObjects.notifyAll();
    }
}

public void saveSomethingImportantToDatabase(Object theObjectIwantToSave) throws InterruptedException {
    try {
        lockObject(theObjectIwantToSave);

        if (!methodThatChecksThatObjectAlreadyExists(theObjectIwantToSave)) {
            storeMyObject(theObjectIwantToSave);
        }
        commit();
    } finally {
        unlockObject(theObjectIwantToSave);
    }
}
  • 您必须为对象的类正确覆盖方法'等于''hashCode'。如果对象内部有唯一的 id (字符串或数字),则只需检查此ID而不是整个对象,而无需覆盖“等于”和“哈希码”。
  • 最终尝试-非常重要-即使操作引发异常,您也必须保证在操作后解锁等待线程。
  • 如果您的后端分布在多台服务器上,则此方法将不起作用。