I just finished reading Java Concurrency in Practice (JCIP) (buy on Amazon) and realised that every piece of threaded code I’ve ever written is probably broken. The book is pretty tough reading and covers the concurrent libraries in quite a bit of detail (the implementation of nonblocking concurrency in ConcurrentLinkedQueue is possibly the most mind-blowing code I’ve ever seen). The hardest hitting lesson that I learnt was about visibility of fields across threads, and in particular across multiprocessor/multicore systems.
In the following code snippet, we have a Runnable that keeps doing stuff until another thread calls stopIt(). If you think it is thread-safe, then you need to read JCIP.
boolean stop = false;
public void run() {
while(!stop){
doStuff();
}
}
public void stopIt(){
stop = true;
}
The reason why it is broken is because of the visibility of the field stop. In the Java Memory Model (JMM), different threads (and in particular, different CPUs) are allowed to take local caches of fields. It may be that the thread running run may never know to re-cache the value of stop and it will run forever. JCIP gives a detailed rundown of when threads take new snapshots of the fields they are accessing, but the simple answer is stop must be made volatile, or it must be read from within a synchronized block.
A simple way to avoid the pitfall is to use the final keyword when declaring fields. Obviously this doesn’t work for primitive types, but there are atomic versions of the primitives in java.util.concurrent.atomic that also provide some additional atomic operations (note that i++ on an int is actually 3 operations, not one, so best use incrementAndGet. If something has been declared final, then the JMM knows that it always has the most up to date reference to the object and that it will never change. This has led to a frenzy of adding final to everything we possibly can in our codebase :-).
This is only the tip of the iceberg in terms of what the book covers… it explores alternatives to using synchronized with emphasis on scalability. One of my personal favourites is the use of ReadWriteLock for making maps/databases block only on writing, with multiple concurrent reads.
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public V get(K key){
readLock.lock();
try {
// do lookup and return
} finally {
// don't forget to unlock
readLock.unlock();
}
}
public void set(K key, V value){
writeLock.lock();
try {
// do setting and return
} finally {
// don't forget to unlock
writeLock.unlock();
}
}
Peter Veentjer wrote:
June 15th, 2007 at 1:16 pm