public class ConcurrentIntegerArray {
private final Lock lock = new ReentrantLock();
private final int[] arr;
public ConcurrentIntegerArray(int size) {
arr = new int[size];
}
public void set(int index, int value) {
lock.lock();
try {
arr[index] = value;
} finally {
lock.unlock();
}
}
public int get(int index) {
lock.lock();
try {
return arr[index];
} finally {
lock.unlock();
}
}
}
In this example, we have an underlying array, arr, and we wrap
accesses to this array in a lock/unlock pair. Once thing you may be wondering is
about memory access semantics. With regular synchronized blocks, the JVM
guarantees that entering and exiting the synchronized block acts as a "memory
barrier", meaning that, for example, where a memory write occurs inside the
synchronized block, this write cannot be re-ordered to occur after
the block is exited (else another thread might not see it). Here, there's no
synchronized, so how does it work? Well, it turns out that a
contract of the Lock interface is that it provides the
same memory barrier behaviour as synchronized. What that
basically means is that we can safely use the lock/unlock action as the
thing that guarnatees that a write by Thread A will be seen by Thread B.
In case it's not obvious: once we've chosen a Lock as our
method of synchronizing access to a particular variable, we should make sure
we use only that same lock object for all accesses.
In particular, we can't mix synchronization via a Lock with
synchronization via synchronized and expect it to work! I mention
this mainly because it's an accident that could happen if you are adapting
legacy code to use explicit locks instead of synchronized blocks2.
(When constructing the object,
it is OK to rely on final here to make sure
that the arr reference set by one thread is visible to other
threads accessing the get/set methods.)
Next...
Now we've seen the basics of using explicit locks, we look at some of
the benefits, starting with timed
and interruptible locking.
Notes:
1. A lock is said to be re-entrant if the owning thread can
call its lock method multiple times without blocking. To release the lock, the owning
thread must call the unlock method as many times as it called the lock method.
2. Of course, if you've used good encapsulation, all of the locking needed by
a particular class will be internal to that class and it'll be easy to
find all the places where synchronized is used, won't it...?
If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.
Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.