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:
assert foo.getOther().getOther() == foo
fails when another thread is performing setOther
concurrently. setOther
and no other other thread overrides the value, getOther
immediately returns the new value for that thread. getOther
, it will never again receive the old value (unless it is set again). Also nice to have:
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.
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:
x.oRef.compareAndSet(y, null)
. 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.