How to lock multiple resources in java multithreading

  • A+
Category:Languages

I have a requirement of locking several objects in one method in my java class. For an example look at the following class:

public class CounterMultiplexer {      private int counter =0;     private int multiPlexer =5;     private Object mutex = new Object();      public void calculate(){          synchronized(mutex){             counter ++;             multiPlexer = multiPlexer*counter;         }      }     public int getCounter(){       return counter;    }    public int getMux(){       return multiPlexer;    } } 

In the above code, I have two resources that could access by a more than one thread. Those two resources are counter and the multiPlexer properties. As you can see in the above code I have locked both the resources using a mutex.

Is this way of locking is correct? Do I need to use nested Synchronized statements to lock both resources inside the calculate method?

 


So you've got the idea of mutex (and atomicity) correct. However there's an additional wrinkle in the Java memory model which is visibility that you have to take into consideration.

Basically, both reads and writes must be synchronized, or the read is not guaranteed to see the write. For your getters, it would be very easy for the JIT to hoist those values into a register and never re-read them, meaning the value written would never be seen. This is called a data race because the order of the write and the read cannot be guaranteed.

To break the data race, you have to use memory ordering semantics. This boils down to synchronizing both the reads and the writes. And you have to do this every time you need to use synchronization anywhere, not just in the specific case you have above.

You could use almost any method (like AtomicInteger) but probably the easiest is either to re-use the mutex you already have, or to make the two primitive values volatile. Either works, but you must use at least one.

public class CounterMultiplexer {      private int counter =0;     private int multiPlexer =5;     private Object mutex = new Object();      public void claculate(){          synchronized(mutex){             counter ++;             multiPlexer = multiPlexer*counter;         }      }     public int getCounter(){       synchronized(mutex){         return counter;      }    }     public int getMux(){       synchronized(mutex){         return multiPlexer;       }    } } 

So to get into this more, we have to read the spec. You can also get Brian Goetz's Java Concurrency in Practice which I highly recommend because he covers this sort of thing in detail and with simple examples that make it very clear that you must syncrhonize on both reads and writes, always.

The relevant section of the spec is Chapter 17, and in particular section 17.4 Memory Model.

Just to quote the relevant parts:

The Java programming language memory model works by examining each read in an execution trace and checking that the write observed by that read is valid according to certain rules.

That bit is important. Each read is checked. The model doesn't work by checking the writes alone and then assuming the reads can see the write.

Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second.

The happens-before is what allows reads to see a write. Without it, the JVM is free to optimize your program in ways that might preclude seeing the write (like hoisting a value into a register).

The happens-before relation defines when data races take place.

A set of synchronization edges, S, is sufficient if it is the minimal set such that the transitive closure of S with the program order determines all of the happens-before edges in the execution. This set is unique.

It follows from the above definitions that:

An unlock on a monitor happens-before every subsequent lock on that monitor.

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

So happens-before defines when a data race does (or does not) take place. How volatile works I think is obvious from the description above. For a monitor (your mutex), it's important to note that happens-before is established by a unlock followed by a later lock, so to establish happens-before for the read, you do need to lock the monitor again just before the read.

We say that a read r of a variable v is allowed to observe a write w to v if, in the happens-before partial order of the execution trace:

r is not ordered before w (i.e., it is not the case that hb(r, w)), and

there is no intervening write w' to v (i.e. no write w' to v such that hb(w, w') and hb(w', r)).

Informally, a read r is allowed to see the result of a write w if there is no happens-before ordering to prevent that read.

"Allowed to observe" means the read actually will see the write. So happens-before is what we need to see the write, and either the lock (mutex in your program) or volatile will work.

There's lots more (other things cause happens-before) and there's the API too with classes in java.utli.concurrent that will also cause memory ordering (and visibility) semantics. But there's the gory details on your program.

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: