Sergey Orlov
Sergey Orlov

Reputation: 111

Can I synchronize method by parameter

Can I synchronize method by parameter?

For example - I get person to some method and I want to do some operation for person, but if few thread call this method for the same person I want to do it one by one.

private void dosomething(Long id, Person person) {
    dosomethingelse(id, person);
}

How to call dosomethingelse (id, person) only for the same id one by one? but I want that this code for different id-s can be called multithreadly

I wrote this code, but maybe something wrong here or something can be better.

public static class LatchByValue <T> {
    public void latch(T value, ConsumerWithException<T> consummer) throws Exception {
        CountDownLatch latch = new CountDownLatch(1);
        try {
            CountDownLatch previousLatch = null;
            // we are checking if another thread is already calling this method with the same id
            // if sync has CountDownLatch so another thread is already calling this method 
            // or we put our latch and go on
            while ((previousLatch = sync.putIfAbsent(value, latch)) != null) {
                try {
                    // we are waiting for another thread, we are waiting for all threads that put their latch before our thread
                    previousLatch.await();
                } catch (InterruptedException e) {
                    return;
                }
            }
            consummer.accept(value);
        } finally {
            latch.countDown();
            sync.remove(value, latch);
        } 
    }
    private ConcurrentHashMap<T, CountDownLatch> sync = new ConcurrentHashMap<>();
}

Example:

LatchByValue<Long> latch = new LatchByValue<>();

private void dosomething(Long id, Person person) {
     latch.latch(
        id,
        currentId -> { dosomethingelse(currentId, person); }
     );
}

Upvotes: 3

Views: 2098

Answers (3)

Anton Fil
Anton Fil

Reputation: 305

    private static final Set<Long> lockedIds = new HashSet<>();

    private void lock(Long id) throws InterruptedException {
        synchronized (lockedIds) {
            while (!lockedIds.add(id)) {
                lockedIds.wait();
            }
        }
    }

    private void unlock(Long id) {
        synchronized (lockedIds) {
            lockedIds.remove(id);
            lockedIds.notifyAll();
        }
    }

    public void doSomething(Long id) throws InterruptedException {
        try {
            lock(id);

            //Put your code here.
            //For different ids it is executed in parallel.
            //For equal ids it is executed synchronously.

        } finally {
            unlock(id);
        }
    }
  • id can be not only an 'Long' but any class with correctly overridden 'equals' and 'hashCode' methods.
  • try-finally - is very important - you must guarantee to unlock waiting threads after your operation even if your operation threw exception.
  • It will not work if your back-end is distributed across multiple servers/JVMs.

Upvotes: 0

Adam Kotwasinski
Adam Kotwasinski

Reputation: 4574

You can use synchronized keyword on the parameter passed (culprit: it cannot be null!). And that also allows you to stop worrying about re-acquiring the lock (it's reentrant).

So the implementation would look like:

private void doSomething(Long id, Person person) {
  synchronized (person) {
    // do something
  }
}

Remember that any other accesses (not in doSomething call) also would need to have the synchronization block, e.g.:

// another method, unrelated, but does something with 'person'
private void doSomethingElse(Person person, ... /* other arguments */) {
  synchronized (person) {
    // do something
  }
}

It would be good document (in Person's javadoc) that the user needs to acquire the lock for that object.


If you want to provide a critical section for <id, person> tuple, you'd need to change your API a bit - and then pass that object around in your application.

private void doSomething(IdAndPerson idAndPerson) {
  synchronized (idAndPerson) {
    // do something
  }
}

class IdAndPerson {
    private final Long id;
    private final Person person;
    // constructor etc.
}

Upvotes: 0

assylias
assylias

Reputation: 328775

Problem with using a CountdownLatch is that you can't "increment" the count so you need to replace the existing latch when it's been used, which complicates the code.

You could instead use a Semaphore with one permit which would allow you to do the same thing but in a simpler way.

Semaphore s = sync.computeIfAbsent(value, x -> new Semaphore(1, true));
s.acquire(); //this blocks and throws InterruptedException, which you need to handle
try {
  consummer.accept(value);
} finally {
  s.release();
}

Upvotes: 2

Related Questions