I found answers only for synchronization on one String object, but not two.
It's not a real task, but an assignment. I have SomeLibrary that transfers money from one account to another. I can't access Account object to lock on it. I can only use SomeLibrary.transfer(String from, String to), which is not thread safe. I have method with account ID's as strings. I need to lock on both of these strings without deadlocks.
What I did so far is:
Created new Strings with .intern method (String fr = from.intern()). But it's bad practice and I'm not allowed to use this method. But it worked.
Created new String from old (String fr = new String(from)). This also worked (I didn't have deadlocks) but I have suspicions about this solution.
Is there other way to lock on both Strings?
I tried to use ConcurrentHashMap and put Strings there, but it didn't work.
May be there is a way to put Strings inside some objects, but where these objects should be created? I can create them inside transfer() but synchronization on a local variable is not good practice either.
My method is:
public void transfer(String from, String to, int amount) {
String fr = new String(from);
String too = new String(to);
int fromHash = System.identityHashCode(fr);
int toHash = System.identityHashCode(too);
if (fromHash < toHash) {
synchronizedTransfer(from, to, amount, fr, too);
} else if (fromHash > toHash) {
synchronizedTransfer(to, from, amount, too, fr);
} else {
synchronized (tieLock) {
synchronizedTransfer(from, to, amount, fr, too);
}
}
}
private void synchronizedTransfer(String from, String to, int amount, String fr, String too) {
synchronized (fr) {
synchronized (too) {
SomeLibrary.transfer(from, to);
}
}
}
CodePudding user response:
You can synchronize on both objects by using nested synchronized blocks and a data structure in which dedicated synchronization objects are stored. To prevent any deadlocks I would compare the two Strings lexicographically:
private static final Hashtable<String, Object> syncTable = new Hashtable<>();
private static final Object syncOnEquals = new Object();
public void transfer(String from, String to, int amount) {
Object syncFrom = syncTable.computeIfAbsent(from, s -> new Object());
Object syncTo = syncTable.computeIfAbsent(to, s -> new Object());
int comparingResult = from.compareTo(to);
if (comparingResult > 0) {
synchronized (syncFrom) {
synchronized (syncTo) {
SomeLibrary.transfer(from, to);
}
}
} else if (comparingResult < 0) {
synchronized (syncTo) {
synchronized (syncFrom) {
SomeLibrary.transfer(from, to);
}
}
} else {
synchronized (syncOnEquals) {
SomeLibrary.transfer(from, to);
}
}
}
If you are not familiar with lambda expressions:
Object syncFrom = syncTable.computeIfAbsent(from, s -> new Object());
is equivalent to
Object syncFrom;
synchronized (syncTable) {
syncFrom = syncTable.get(from);
if (syncFrom == null) {
syncFrom = new Object();
syncTable.put(from, syncFrom);
}
}
CodePudding user response:
What must be clear, is that for a transfer (from, to) all (from, ), (, from), (*, to), (to, *) transfers might need to be guarded.
The only way to do that is to synchronize of from and synchronize on to. This could cause a dead lock when there is a transfer (to, from). For that one can order from and to so that synchronisation AAA comes before synchronisation BBB.
An other approach is to have atomic deposit (from, -amount), deposit (to, amount) and do all in a transaction that can be rolled back.
Now on the synchronisation object: this must be a unique Object instance. You could put use the account ID String, as put in a Map
(Set
). As such you have a unique object as key in the map. Of course the map operations suffer from concurrency too, use: Collections.synchronizedMap
. And you need to update it, add/remove account IDs.
As transactions are a separate topic, nest two synchronisations in canonical order.