锁定特定对象的 Java 线程

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

I have a web application and I am using Oracle database and I have a method basically like this:

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个线程进入这个方法时出现问题,同时检查当然还没有任何东西,然后他们都可以提交事务,创建一个重复的对象.

Right now there is no synchronization of any kind so n threads can of course access this method freely, the problem arises when 2 threads enter this method both check and of course there is nothing just yet, and then they can both commit the transaction, creating a duplicate object.

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

I do not want to solve this with a unique key identifier in my Database, because I don't think I should be catching that SQLException.

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

I also cannot check right before the commit, because there are several checks not only 1, which would take a considerable amount of time.

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

My experience with locks and threads is limited, but my idea is basically to lock this code on the object that it is receiving. I don't know if for example say I receive an Integer Object, and I lock on my Integer with value 1, would that only prevent threads with another Integer with value 1 from entering, and all the other threads with value != 1 can enter freely?, is this how it works?.

如果这就是它的工作原理,那么锁对象是如何比较的?如何确定它们实际上是同一个对象?一篇关于这方面的好文章也将不胜感激.

Also if this is how it works, how is the lock object compared? how is it determined that they are in fact the same object?. A good article on this would also be appreciated.

你会如何解决这个问题?

How would you solve this?.

推荐答案

你的想法不错.这是简单/幼稚的版本,但不太可能工作:

Your idea is a good one. This is the simplistic/naive version, but it's unlikely to work:

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.
    }
}

此代码使用对象本身作为锁.但它必须是 same 对象(即 objectInThreadA == objectInThreadB)才能工作.如果两个线程在一个对象上操作,该对象是彼此的副本 - 例如,具有相同的id",那么您需要同步整个方法:

This code uses the object itself as the lock. But it has to be the same object (ie objectInThreadA == objectInThreadB) if it's to work. If two threads are operating on an object that is a copy of each other - ie has the same "id" for example, then you'll need to either synchronize the whole method:

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

这当然会大大降低并发性(使用该方法吞吐量将一次下降到一个线程 - 应避免).

which will of course greatly reduce concurrency (throughput will drop to one thread at a time using the method - to be avoided).

或者根据保存对象想办法获取相同的锁对象,像这样的做法:

Or find a way to get the same lock object based on the save object, like this approach:

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()) 才能正常工作.此外,由于 java 的 int)/guide/language/autoboxing.html" rel="nofollow noreferrer">自动装箱.

This last version it the recommended one: It will ensure that two save objects that share the same "id" are locked with the same lock object - the method ConcurrentHashMap.putIfAbsent() is threadsafe, so "this will work", and it requires only that objectInThreadA.getId().equals(objectInThreadB.getId()) to work properly. Also, the datatype of getId() can be anything, including primitives (eg int) due to java's autoboxing.

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

If you override equals() and hashcode() for your object, then you could use the object itself instead of object.getId(), and that would be an improvement (Thanks @TheCapn for pointing this out)

此解决方案仅适用于一个 JVM.如果您的服务器是集群的,那么完全不同的球赛和 java 的锁定机制将无济于事.您必须使用集群锁定解决方案,这超出了此答案的范围.

This solution will only work with in one JVM. If your servers are clustered, that a whole different ball game and java's locking mechanism will not help you. You'll have to use a clustered locking solution, which is beyond the scope of this answer.

相关文章