3

What is a good way to implement thread-safe bidirectional associations? Is there maybe a good library or code generator?

Here is a non thread-safe example:

class Foo {

    private Foo other;

    public Foo getOther() {
        return other;
    }

    public void setOther(Foo other) {
        this.setOtherSecretly(other);
        other.setotherSecretly(this);
    }

    void setOtherSecretly(Foo other) {
        if (this.other != null) this.other.other = null;
        this.other = other;
    }
}

My requirements for thread-safety are:

  • No deadlocks
  • Eventual consistency (When all threads stop modifying the objects, a consistent state is eventually reached. I.e., it is acceptable that assert foo.getOther().getOther() == foo fails when another thread is performing setOther concurrently.
  • Sequential behaviour. If a thread performs setOther and no other other thread overrides the value, getOther immediately returns the new value for that thread.
  • No traveling back in time. Once a thread observed a new value with getOther, it will never again receive the old value (unless it is set again).

Also nice to have:

  • Low contention, especially no global lock. The solution should scale well.
  • As little synchronization overhead as possible. It should have reasonable performance for a single thread.
  • Low memory overhead. When an object has 5 associations, I don't want 3 additional fields per association. Local variables in setters are ok.

My application will have 16 threads working on about 5.000 objects of several classes.

I couldn't come up with a solution yet (no, this is not homework), so any input (ideas, articles, code) is welcome.

Cephalopod
  • 14,632
  • 7
  • 51
  • 70
  • this seems very difficult, if possible. what if you do it with a global lock, and see if the performance is acceptable? – irreputable Feb 21 '11 at 20:30

6 Answers6

2

Google Guava does this for you: BiMap.

For example:

BiMap<Integer, String> bimap = Synchronized.biMap(HashBiMap.create(), someMutexObject);
bimap.put(1, "one");
bimap.put(2, "two");

bimap.get(1); // returns "one"
bimap.inverse().get("one") // returns 1

someMutexObject can be any object you would want to synchronize on.

sjr
  • 9,769
  • 1
  • 25
  • 36
1

You can associate each object to their own lock and then set the other while acquiring both locks. For instance. To avoid deadlock you can use lock ordering

class Foo extends ReentrantLock {

    private static final AtomicInteger order = new AtomicInteger(0);

    final int id = order.incrementAndGet();

    private Foo other;

    public Foo getOther() {
        return other;
    }

    public void setOther(Foo other) {
        if (id > other.id) {
            other.lock();
            try {
                this.lock();
                try {

                    // assign here
                } finally {
                    this.unlock();
                }
            } finally {
                other.unlock();
            }
        } else if (id < other.id) {
            this.lock();
            try {
                other.lock();
                try {

                    // assign here
                } finally {
                    other.unlock();
                }
            } finally {
                this.unlock();
            }
        }
    }
}
John Vint
  • 39,695
  • 7
  • 78
  • 108
  • The basic idea is sound, but keep in mind that if you need to undo existing associations as well (as the original sample code implies), you need to acquire locks on potentially two more objects. And since you need to read the data before acquiring locks, to determine the order in which to acquire locks, then you need to re-verify the relationships after acquiring the locks before making any changes. I think this is all feasible though. – Dave Costa Feb 21 '11 at 19:51
0

Try this, will allow reading while no writing is done.

ReentrantReadWriteLock

Yochai Timmer
  • 48,127
  • 24
  • 147
  • 185
0

The other alternative is to simply make the other reference(s) volatile. That will meet your requirement and your nice-to-haves.

Lawrence Dol
  • 63,018
  • 25
  • 139
  • 189
  • 1
    Is that sufficient to prevent the following scenario? Three instances A,B,C. Thread1 calls A.setOther(B); Thread2 calls A.setOther(C). With no synchronization or locking, the assignments could happen in this order: A.other=B; A.other=C; C.other=A; B.other=A. Leaving B incorrectly pointing to A as its other when A points to C as its other. – Dave Costa Feb 21 '11 at 19:20
  • It definitevly is not sufficient to ensure consistency. – Cephalopod Feb 21 '11 at 19:47
  • No it's not safe for that. I was not expecting, from your question, that anymore than 1 thread would actually call setOther() - to do so seems contrived and nonsensical - that is, it indicates a probable broken design. – Lawrence Dol Feb 21 '11 at 20:51
0

I can think of an static member to work as a monitor. but maybe this is what you consider 'global' lock.

class Foo {

    private static final Object MONITOR = new Object();
    private Foo other;

    public Foo getOther() {
        synchronized(MONITOR){
            return other;
        }
    }

    public void setOther(Foo other) {
        synchronized(MONITOR){
            this.setOtherSecretly(other);
            other.setotherSecretly(this);
        }
    }

    void setOtherSecretly(Foo other) {
        if (this.other != null) this.other.other = null;
        this.other = other;
    }
}
Sven
  • 166
  • 4
0

This turns out to be a really hard problem! (Nice!) Using a global lock would be too easy, and probably too slow. I think I have a lock-free version--which I'll get into below--but I wouldn't put too much faith in it being perfect. It's hard to reason about all the possible interleavings.

As it turns out, this is a perfect use case for transactional memory! Just mark the whole block as atomic and modify whatever you want! You might look at Deuce STM, though I don't know how fast it might be. If only the best systems didn't need custom hardware...

Anyway, after thinking through this problem for a while, I think I came up with a version that bypasses locks using Java's AtomicReference. First, the code:

class Foo {

    private AtomicReference<Foo> oRef = new AtomicReference<Foo>;

    private static final AtomicInteger order = new AtomicInteger(0);
    private final int id = order.incrementAndGet();

    private static bool break(Foo x, Foo y) {
        if (x.id > y.id)
            return break(y, x);

        return x.oRef.compareAndSet(y, null) &&
               y.oRef.compareAndSet(x, null);
    }

    public void setOther(Foo f) {
        if (f != null && f.id > id) {
            f.setOther(this);
            return;
        }
        do {
            Foo other = oRef.get();
            if (other == f)
                break;

            if (other != null && !break(this, other))
                continue;

            if (f == null)
                break;

            Foo fother = f.oRef.get();
            if (fother != null && !break(f, fother))
                continue;

            if (!f.oRef.compareAndSet(null, this))
                continue;
            if (!oRef.compareAndSet(null, f)) {
                f.oRef.set(null);
                continue;
            }
        } while (false);
    }
}

Key points:

  • If there are no concurrent accesses to any of the affected Foos (at most 4), the setter makes one pass through the loop modifying the relevant pointers.
  • In the presence of concurrent setters, some of the setters might fail and retry.
  • If multiple threads try to break a relationship concurrently, only one thread will succeed executing x.oRef.compareAndSet(y, null).
  • If f.oRef.compareAndSet(null, f) succeeds, no other thread will be able to break the half-established relationship in break(). Then if oRef.compareAndSet(null, f) succeeds, the operation is complete. If it fails, f.oRef can be reset and everyone retries.
Karmastan
  • 5,618
  • 18
  • 24
  • If you have a guaranteed-unique-and-consistent ranking available (I'd be inclined to use a `long` rather than `int` to avoid any possibility of overflow), then you can safely lock the items to be adjusted (if locks locks are acquired in order of rank and released in the order opposite that of acquisition, deadlock is impossible). Reasoning about code using locks may be easier than reasoning about code using compare-and-set. – supercat Feb 18 '14 at 17:09