Java threads locking on a specific object

前端 未结 10 838
抹茶落季
抹茶落季 2020-12-12 00:06

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

public static void saveSomethingImportantToDataBase(Object t         


        
相关标签:
10条回答
  • 2020-12-12 00:21
    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.
      }
    }
    

    The synchronized keyword locks the object you want so that no other method could access it.

    0 讨论(0)
  • 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.
        }
    }
    

    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
    }
    

    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.

    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)

    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.

    0 讨论(0)
  • 2020-12-12 00:23
    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);
        }
    }
    
    • You must correctly override methods 'equals' and 'hashCode' for your objects' classes. If you have unique id (String or Number) inside your object then you can just check this id instead of the whole object and no need to override 'equals' and 'hashCode'.
    • try-finally - is very important - you must guarantee to unlock waiting threads after your operation even if your operation threw exception.
    • This approach will not work if your back-end is distributed across multiple servers.
    0 讨论(0)
  • 2020-12-12 00:24

    Here is an option adapted from And360's comment on Bohemian's answer, that tries to avoid race conditions, etc. Though I prefer my other answer to this question over this one, slightly:

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

    You could split these out into helper methods "get lock object" and "release lock" or what not, as well, to cleanup the code. This way feels a little more kludgey than my other answer.

    0 讨论(0)
  • 2020-12-12 00:27

    Bohemian's answer seems to have race condition problems if one thread is in the synchronized section while another thread removes the synchro-object from the Map, etc. So here is an alternative that leverages WeakRef's.

    // 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!
    }
    

    And a more concrete example of how to use the above style system:

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

    Then use it elsewhere like this:

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

    The reason this works is basically that if two objects with matching keys enter the critical block, the second will retrieve the lock the first is already using (or the one that is left behind and hasn't been GC'ed yet). However if it is unused, both will have left the method behind and removed their references to the lock object, so it is safely collected.

    If you have a limited "known size" of synchronization points you want to use (one that doesn't have to decrease in size eventually), you could probably avoid using a HashMap and use a ConcurrentHashMap instead, with its putIfAbsent method which might be easier to understand.

    0 讨论(0)
  • 2020-12-12 00:27

    I don't think you have any choice but to take one of the solutions that you do not seem to want to do.

    In your case, I don't think any type of synchronization on the objectYouWantToSave is going to work since they are based on web requests. Therefore each request (on its own thread) is most likely going to have it's own instance of the object. Even though they might be considered logically equal, that doesn't matter for synchronization.

    0 讨论(0)
提交回复
热议问题