0

my question is about synchronisation and preventing deadlocks when using threads. In this example an object simply holds an integer variable and multiple threads call swapValue on those objects.

public class Data {

    private long value;

    public Data(long value) {
        this.value = value;
    }
    public synchronized long getValue() {
        return value;
    }
    public synchronized void setValue(long value) {
        this.value = value;
    }

    public void swapValue(Data other) {
        long temp = getValue();
        long newValue = other.getValue();
        setValue(newValue);
        other.setValue(temp);
    }
}

The swapValue method should be thread safe and should not skip swapping the values if the resources are not available. Simply using the synchronized keyword on the method signature will result in a deadlock. I came up with this (apparently) working solution, which is only based on the probability that one thread unlocks its resource and the other tries to claim it while the resource is still unlocked.

private Lock lock = new ReentrantLock();

...

public void swapValue(Data other) {
    lock.lock();
    while(!other.lock.tryLock())
    {
        lock.unlock();
        lock.lock();
    }

    long temp = getValue();
    long newValue = other.getValue();
    setValue(newValue);
    other.setValue(temp);
    
    other.lock.unlock();
    lock.unlock();

}

To me this looks like a hack. Is this a common solution for these kind of problems? Are there solutions that are "more deterministic" in their behaviour and also applicable in practice?

Neran
  • 55
  • 7

3 Answers3

2

There are two issues at play here:

  • First, mixing Data.lock with the built-in lock used by the synchronized keyword
  • Second, inconsistent locking order among four (!) locks - this.lock, other.lock, the built-in lock of this, and the built-in lock of other

Even without synchronized, a.swapValue(b) and b.swapValue(a) can deadlock unless you use your approach to try to spin while locking and unlocking, which is inefficient.

One approach that you could take is add a field with some kind of final unique ID to each Data object - when swapping data of two objects, lock the one with a lower ID before the one with the higher ID, regardless of which is this and which is other. Note that System.identityHashCode is unfortunately not unique so it can't be easily used here.

The unlock ordering isn't critical here, but unlocking in the reverse order of locking is generally a good practice to follow where possible.

nanofarad
  • 40,330
  • 4
  • 86
  • 117
  • I've edited out the claim (added in a suggested edit) that the wrong unlock order can deadlock. Releasing locks should not itself block, so the thread that's unlocking ought to be able to make progress and release the next thread. – nanofarad Feb 09 '23 at 23:51
  • I assumed too much. Please accept my apology for that. But I still believe the 'good practice' you reference is actually a required part of lock ordering. – Queeg Feb 10 '23 at 14:33
  • @Queeg I agree that unlock ordering becomes important when you have *reacquisition* while under a subset of the locks. I can't think of a locking sequence that deadlocks for *this* algorithm when the releases are out of order. – nanofarad Feb 10 '23 at 14:36
2

@Nanofarad has the right idea: Give every Data instance a unique, permanent numeric ID, and then use those IDs to decide which object to lock first. Here's what that might look like in practice:

private static void lockBoth(Data a, Data b) {
    Lock first = a.lock;
    Lock second = b.lock;
    if (a.getID() < b.getID()) {
        first = b.lock;
        second = a.lock;
    }
    first.lock();
    second.lock();
}

private static void unlockBoth(Data a, Data b) {
    a.lock.unlock();
    b.lock.unlock();

    // Note: @Queeg suggests in comments below that in the general case,
    // it would be good practice to make this routine always unlock the
    // two locks in the order opposite to which `lockBoth()` locked them.
    // See https://stackoverflow.com/a/8949355/801894 for an explanation.
}

public void swapValue(Data other) {
    lockBoth(this, other);
    ...swap 'em...
    unlockBoth(this, other);
}
Solomon Slow
  • 25,130
  • 5
  • 37
  • 57
  • While the resources to lock are sorted, ensure unlocking happens in the reverse order. – Queeg Feb 09 '23 at 23:12
  • @Queeg, Why? What does it matter which lock is unlocked first? The unlock calls will neither block nor fail. – Solomon Slow Feb 09 '23 at 23:16
  • @Queeg I also wonder the same thing - releasing a lock does not block and does not depend on acquiring other locks. Can you explain your rationale for this claim? (which should have been a comment rather than an edit, as it is not my answer's intent to make that claim). I can imagine that it's *neater* to do it this way, but I am struggling to identify a locking pattern that deadlocks when the *acquisition* order is correct. – nanofarad Feb 09 '23 at 23:51
  • I came across a smiliar post here on SO that proposes a very similar solution taken from a concurrency book "java concurrency in practice". its from 2006, so pretty "old". are the solutions to concurrency problems in that book still very common/up to date as o f now? – Neran Feb 10 '23 at 11:36
  • 1
    @Neran, Books get old. Algorithms don't. It's been years since I cracked open Java Concurrency in Practice, but I think it maybe spent more words discussing how to solve certain problems _in Java_ than it spent on describing the problems themselves. Java has grown much since then, but the underlying ideas still are the same. You want to safely lock multiple objects? That problem and this solution both have been around since before Java was even dreamed of. I don't know if there is a better _architecture_ for solving your problem, but this solution will work for the architecture you have now. – Solomon Slow Feb 10 '23 at 14:07
  • It seems we agree that locking in order is good to prevent deadlocks. The explanation in https://stackoverflow.com/questions/1951275/would-you-explain-lock-ordering/8949355#8949355 shows that if you release locks out of order, lock order breaches may be hidden in the code. But a breach in lock order would mean the prevention of deadlocks done by lock ordering is not in effect. – Queeg Feb 10 '23 at 14:32
  • @Queeg, OK, so in this _specific_ case, there is no "lock order breach...hidden in the code," But I can see how, in general, it would be good hygiene to always unlock the locks in the opposite order to which they were locked. – Solomon Slow Feb 10 '23 at 14:45
-1

In your case, just use AtomicInteger or AtomicLong instead inventing the wheel again. About the synchronization and deadlocks part of your question in general - DO NOT RELY ON PROBABILITY -- it is way too tricky and too easy to get it wrong, unless you're experienced mathematician knowing exactly what youre doing - but even then it is risky. One example when probability is used is UUID, but if computers will get fast enough then the code that shouldn't reasonably break till the end of universe can break in matter of milliseconds or faster, it is better to write code that do not rely on probability, especially concurrent code.

Krzysztof Cichocki
  • 6,294
  • 1
  • 16
  • 32
  • I also thought about the atomic data types. When can atomic types NOT be used? – Neran Feb 09 '23 at 21:55
  • @Neran At least in the most obvious realization, atomics don't actually work for this problem. You can atomically swap a local with an atomic, but you can't swap an with an atomic atomically (see [this](https://stackoverflow.com/a/60556120/1424875)). After some sketching, I found at least one access pattern that starts with Datas 1, 2, 3 and ends up with Datas 2, 3, 2 after two swaps (using atomic accesses) race (because each swap requires multiple atomics). Krzysztof - do you have a particular lock-free algorithm that you're suggesting? – nanofarad Feb 10 '23 at 00:03
  • If we have datas A, B, C, load B -> tempvar 1, load B -> tempvar 2, swap tempvar 1 and A, swap tempvar 2 with C, store tempvar 1 to B, store tempvar 2 to B ends up with two copies of B and one of C, with the copy of A lost. – nanofarad Feb 10 '23 at 00:12