Java Concurrency – Part 3 : Synchronization with intrinsic locks
<After learning how to create threads and manipulate them, it's time to go to most important things : synchronization.
Synchronization is a way to make some code thread safe. A code that can be accessed by multiple threads must be made thread safe. Thread Safe describe some code that can be called from multiple threads without corrupting the state of the object or simply doing the thing the code must do in right order.
For example, we can take this little class :
public class Example { private int value = 0; public int getNextValue(){ return value++; } }
It's really simple and works well with one thread, but absolutely not with multiple threads. An increment like this is not a simple action, but three actions :
- Read the current value of "value"
- Add one to the current value
- Write that new value to "value"
Normally, if you have two threads invoking the getNextValue(), you can think that the first will get 1 and the next will get 2, but it is possible that the two threads get the value 1. Imagine this situation :
- Thread 1 : read the value, get 0, add 1, so value = 1
- Thread 2 : read the value, get 0, add 1, so value = 1
- Thread 1 : write 1 to the field value and return 1
- Thread 2 : write 1 to the field value and return 1
These situations come from what we call interleaving. Interleaving describe the possible situations of several threads executing some statements. Only for three operations and two threads, there is a lot of possible interleavings.
So we must made the operations atomic to works with multiple threads. In Java, the first way to make that is to use a lock. All Java objects contains an intrinsic locks, we'll use that lock to make methods or statement atomic. When a thread has a lock, no other thread can acquire it and must wait for the first thread to release the lock. To acquire the lock, you have to use the synchronized keyword to automatically acquire and release a lock for a code. You can add the synchronized keyword to a method to acquire the lock before invoking the method and release it after the method execution. You can refactor the getNextValue() method using the synchronized keyword :
public class Example { private int value = 0; public synchronized int getNextValue(){ return value++; } }
With that, you have the guarantee that only thread can execute the method at the same time. The used lock is the intrinsic lock of the instance. If the method is static, the used lock is the Class object of Example. If you have two methods with the synchronized keyword, only one method of the two will be executed at the same time because the same lock is used for the two methods. You can also write it using a synchronized block :
public class Example { private int value = 0; public int getNextValue() { synchronized (this) { return value++; } } }
This is exactly the same as using the synchronized keyword on the method signature. Using synchronized blocks, you can choose the lock to block on. By example, if you don't want to use the intrinsic lock of the current object but an other object, you can use an other object just as a lock :
public class Example { private int value = 0; private final Object lock = new Object(); public int getNextValue() { synchronized (lock) { return value++; } } }
The result is the same but has one difference, the lock is internal to the object so no other code can use the lock. With complex classes, it not rare to use several locks to provide thread safety on the class.
There is an other issue with multiple threads : the visibility of the variables. This seems when a change made by a thread is visible by an other thread. For performance improvements, the Java compiler and virtual machines can made some improvements using registers and cache. By default, you have no guarantee that a change made by a thread is visible to an other thread. To make a change visible to an other thread, you must use synchronized blocks to ensure visibility of the change. You must use synchronized blocks for the read and for the write of the shared values. You must make that for every read/write of a value shared between multiple threads.
You can also use the volatile keyword on the field to ensure the visibility of read/write between multiple threads. The volatile keyword ensure only visibility, not atomicity. The synchronized blocks ensure visibility and atomicity. So you can use the volatile keyword on fields that doesn't need atomicity (if you make only read and write to the field without depending on the current value of the field by example).
You can also note that this simple example can be solved using AtomicInteger, but that will be covered later in an other part of the posts.
Pay attention that trying to solve thread safety on a problem can add new issues of deadlock. By example, if thread A owns the lock 1 and are waiting for the lock 2 and if lock 2 is acquired by thread B who waits on lock 1, there is a deadlock. Your program is dead. So you have to pay great attention to the locks.
There is several rules that we must keep in mind when using locks :
- Every mutable fields shared between multiple threads must be guarded with a lock or made volatile, if you only need visibility
- Synchronize only the operations that must synchronized, this improve the performances. But don't synchronize too few operations. Try to keep the lock only for short operations.
- Always know which locks are acquired and when there are acquired and by which thread
- An immutable object is always thread safe
Here we are, I hope that this post helps you to understand thread safety and how to achieve it using intrinsic locks. In the next posts, we'll see another synchronization methods.
Comments
Comments powered by Disqus