简体   繁体   中英

Threadsafe bidirectional association in Java

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. Ie, 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.

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.

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

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

ReentrantReadWriteLock

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

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM