有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

多线程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,这将花费相当长的时间

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

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

你将如何解决这个问题


共 (6) 个答案

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

    synchronized关键字会锁定所需的对象,以便其他方法无法访问它

  2. # 2 楼答案

    你的想法很好。这是一个过于简单/幼稚的版本,但不太可能奏效:

    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)。如果两个线程在一个对象上运行,而该对象是彼此的副本(例如,ie具有相同的“id”),则需要同步整个方法:

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

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

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

    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的锁定机制对您没有帮助。您必须使用集群锁定解决方案,这超出了本答案的范围

  3. # 3 楼答案

    如果你能忍受偶尔的过度同步(即不需要时按顺序完成的工作),尝试以下方法:

    1. 创建带有锁定对象的表。桌子越大,越不容易发生过度同步
    2. 对id应用一些哈希函数来计算表索引。如果您的id是数字,您可以只使用一个余数(模)函数,如果它是字符串,则使用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
      }
    }
    
  4. # 4 楼答案

    以下是根据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);
            }
        }
    }
    

    可以将这些方法拆分为助手方法“get lock object”和“release lock”或其他方法来清理代码。这种方式比我的other answer感觉有点笨拙

  5. # 5 楼答案

    我的观点是,你并没有遇到真正的线程问题

    最好让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

  6. # 6 楼答案

    波希米亚人的答案似乎有种族条件的问题,如果一个线程在同步部分,而另一个线程从地图上删除同步对象,等等。所以这里有一个替代方案,利用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
      }
    ...
    

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

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