Recommended: Sing it, brah! 5 fabulous songs for developers
JW's Top 5
Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs
Page 2 of 5
You can implement the two-lock strategy by synchronizing on something other than the containing object. Rather than synchronizing the methods, you can define two local variables to use as locks and synchronize on them:
1| class Complicated_and_thread_safe
2| {
3| private long a, b;
4| private long x, y;
5|
6| private Object ab_lock = new int[];
7| private Object xy_lock = new int[];
8|
9| // partition 1, functions use a and/or b
10|
11| public void use_a() { synchronized(ab_lock){
/*...*/ } }
12| public void use_b() { synchronized(ab_lock){
/*...*/ } }
13| public void use_a_and_b(){ synchronized(ab_lock){
/*...*/ } }
14|
15| // partition 2, functions use x and/or y
16|
17| public void use_x() { synchronized(xy_lock){
/*...*/ } }
18| public void use_y() { synchronized(xy_lock){
/*...*/ } }
19| public void use_x_and_y(){ synchronized(xy_lock){
/*...*/ } }
20|
21| // partition 3, functions use a, b, x, and y
22|
23| public void use_everything()
24| { synchronized(ab_lock) // grab both locks
25| { synchronized(xy_lock)
26| { /*...*/
27| }
28| }
29| }
30|
31| public void use_everything_else()
32| { synchronized(ab_lock)
33| { synchronized(xy_lock)
34| { /*...*/
35| }
36| }
37| }
38| }
I haven't synchronized the methods themselves in this example. (Remember, synchronizing a method is effectively the same thing
as wrapping all the code in the method in a synchronized(this){...} block.) Instead, I'm providing a unique lock for each partition (ab_lock and xy_lock) and then explicitly synchronizing on these individual locks.
Java associates locks with objects (instance of some class that extends Object, so I can't use primitive-type variables as locks here. I don't want to spend unnecessary time calling constructors and doing
other initialization operations on complicated objects, however. Consequently, the locks themselves are declared as the simplest
possible Object -- an array.
Arrays in Java are first-class objects: they implicitly extend the Object class. (If you don't believe me, compile the following code:
1| public class foo
2| { static Object ref = new int[]{ 10 };
3|
4| public static void main( String[] args )
5| { System.out.println( ref.toString() );
6| }
7| }
The class compiles just fine. Not only does the implicit cast from the array to Object work (because Object is a base class of all arrays), but the println() correctly invokes the compiler-generated toString() override (which prints absolutely nothing useful -- but you can't have everything). I've used a one-element array for my
lock, rather than something like an Integer, because arrays come into existence very efficiently. For example, there's no constructor call.
In the foregoing example, it's critical that methods that acquire both locks always acquire them in the same order, otherwise we end up in the Wilma-and-Betty deadlock scenario discussed last month. Acquiring multiple locks is a commonplace enough problem that some operating systems have system calls for this purpose. It would be nice to have an easy way to acquire multiple locks, in Java, without having to worry about the order-of-acquisition problem. The remainder of this month's column describes one way to do that.