🔒 Synchronization, Thread-Safety and Locking Techniques in Java and Kotlin

Adib Faramarzi
ProAndroidDev
Published in
9 min readFeb 1, 2019

--

In this article, multiple types of synchronization, locking and thread-safety techniques in Java and Kotlin are explained through interactive examples.

Photo by Tomas Sobek on Unsplash

What is Synchronization?

In a multi-threaded world, we need to access shared objects across threads, and If we do not synchronize our work, unwanted situations can occur.

First let’s see a basic example of why synchronization is needed.

Thread-unsafe code which leads to incorrect result

In the example above, we are launching 1000 coroutines (If you don’t know what they are, just think about them as light-weight threads) on 4 threads, and each of them, increment the global value sharedCounter 1000 times, so the final value of the sharedCounter should be 1000000, but it hardly ever is, unless you get very lucky.

Before we technically explain what is happening here, Imagine there’s one thinking room (the counter) and many people (threads) want to use it but only one person is allowed at a time. This room has a door that when the room is occupied, is closed. What happens in this scenario is, when one person is inside the room, other people can also open the door, come in and use the room. But the door needs a lock!

In the code above, In order to increment the sharedCounter, each thread is trying to do following to internally increment the sharedCounter value:

  1. Get its current value,
  2. Store it in a temp variable, and increment the temp variable by 1,
  3. Save the temp variable to sharedCounter.

But what if one thread gets the current value, and since we are in a multi-threaded world, another thread jumps in, and tries to get the current value? Both of them will get the same value! So each of them increment that value by 1, and store the same value.

This problem can occur in similar other ways, for example, a thread gets past the second level, but before storing it, other threads increment and save the sharedCounter value, and when the first thread jumps in for its third step, it saves an older version of the sharedCounter. This is why the final value is not what we expect.

In order to fix this issue, we need to synchronize the work on this value. There are multiple ways to achieve this which are explained below.

Volatile 🏃

There’s volatile in Java (@Volatile annotation in Kotlin) that can be used on fields. Volatile fields provide memory visibility and guarantee that the value that is being read, comes from the main memory and not the cpu-cache, so the value in cpu-cache is always considered to be dirty, and It has to be fetched again.

Volatiles

As you can see, Volatiles do not help in our scenario, because even though the readers read the latest value from memory, other threads can still read the same value while another thread is trying to increment (step 2, which is not actually writing it). We need the actual increment function to be atomic to the whole code-block.

Volatiles are used in creating thread-safe Singletons in Java by double checking the singleton instance. If you are interested, I recommend reading this article that shows their help to create safe singletons.

Synchronized

One solution is to use Java’s synchronized. There are two types of synchronization available in Java (and Kotlin). Synchronized methods and synchronized statements.

To use synchronized methods, the method needs to be denoted by the synchronized function modifier keyword in Java or @Synchronized annotation in Kotlin.

To use it in the above scenario, we can move our incremental work to a method, and make it synchronized:

Synchronized Method example

As you can see, the output value is always correct. In the room scenario, Synchronized resembles a lock on the door that has only one key that people need to use to open the door and lock it. So when one person (a thread) goes in to use the room, no one else can enter unless the person leaves the room and returns the key.

One thing to note about synchronized methods, is that the access to the whole method is restricted, even when there’s no need for synchronization. Imagine a scenario where the updateCounter is a function that may not need to work with the counter in many of its scenarios. By using synchronized methods, this method is always synchronized, even when the sharedCounter is not updated.

To use synchronization where it is needed, synchronized statements can be used. As an example, let’s only increment the number when our iteration number is even.

Synchronized Statement using Incrementor’s instance as the lock (you can also create another variable inside the class and use that as the lock)

Synchronized statements take any object as their lock, and use the lock to execute their code-block. these blocks have the advantage of being more fine-grained (as they are more scoped).

In the code above, synchronized(this) defines this (the Incrementor’s instance) as the lock object. Any thread that reaches this point, locks the Incrementor instance, does the work defined in the code-block of synchronized and releases the lock.

Atomic Primitives ⚛️

Atomic primitives provide mutating functions for their underlying primitive value which are all atomic thread-safe. In case of AtomicInteger, the increment/decrement functions are provided by the class and they are all treated in an atomic way. Any thread reaching their mutable functions, like incrementAndGet in the below example, are guaranteed to mutate the object in a thread-safe manner.

Locks 🔐

Lock class provides more extensive locking operations than those provided by synchronized methods/statements. In synchronized methods/statements, all locking and unlocking mechanisms have to happen in a block-structured way (inside their method/block). Locks can have the flexibility of being locked or unlocked from anywhere in the code by the programmer.

Let’s see the example with a ReentrantLock:

Locks can also be useful for chain locking. For example, when traversing a LinkedList, on each loop, next item’s lock is acquired and then the current node is unlocked.

Locks also provide helpful timeout functions for acquiring the lock, like Lock.tryLock(long, TimeUnit) which tries to lock the object and has a timeout for when it cannot acquire it.

One important note is that work with Locks should be taken care of with try-catch-finally clauses to ensure safe work with the lock. Also working with locks requires special attention to the detail of locking and unlocking mechanism.

Concurrent Utilities ⏲️

There are multiple concurrent utility classes that can be used to handle concurrency/synchronization.

Photo by Mike Enerio on Unsplash

1. Semaphore

As explained in the previous part, Locks (which can be seen as mutex) are used when a something needs to be done in a linear and atomic way (especially when a resource is shared between multiple consumers). Semaphores take the total number of permits in their constructor (concurrency limit) which can be used to limit the concurrent access to a resource. Semaphores are used when there are signaling required.

For example, when there are producing tasks that signal the semaphore and there are consumer tasks that acquire the semaphore (wait for the signal) when something is ready.

Let’s modify our example to use a binary semaphore (although a semaphore is not a logical way of handling the synchronization issue in the example).

Try changing the value provided to the semaphore to anything more than 1!

Semaphores also provide a setting for being fair (the second argument on their constructor). If they are set to be fair, the thread that waited the longest time gets to acquire the lock that was released in a First-In-First-Out fashion (so there’s a small overhead for Java, to keep the waiting threads in a Queue), If not, any thread can acquire the released lock. If the tasks are identical and it does not matter which thread takes the task, fairness can be turned off for very small performance boost.

Note: tryAcquire() does not perform fairly, even If fairness is set to true (and it will immediately acquire a permit if one is available). If you want to honor the fairness setting, then tryAcquire(0, TimeUnit.SECONDS) can be used.

2. CyclicBarrier

Cyclic barriers are used when we have a fixed number of parties (threads/methods/…) that must wait for each other to reach a common point before continuing execution.

Let’s say we have a few workers that generate a number, and we want to sum all the numbers provided by all workers after they all finish their jobs.

Cyclic Barrier example

In the example above, the code block that was given to the CyclicBarrier’s constructor is run after 3 threads have done their await(). Please note that these await()s are like rendezvous points, all threads will away for the CyclicBarrier to reach the barrier point and will continue their work after they are all there.

Another thing to note about the above example is that createdValues is not synchronized/thread-safe (and can be synchronized by using the synchronizedList that is explained in the next section). You can see this in action by making the Thread.sleep(amount) to a fixed amount (like 500), and the sum value might not always be 6.

CyclicBarriers can be reset() to their initial state. (Note: If there are parties waiting for it’s completion, they will receive a BrokenBarrierException, so threads need to synchronize carefully).

3. CountDownLatch

CountDownLatch is similar to CyclicBarrier, as they are constructed with an initial value and each thread/method can decrease this value to signal its work being done. Other threads can get notified when the count down reaches zero by using await.

CountDownLatch example

4. Concurrent collections

There are multiple ways to handle synchronized collections in Java.

  • Collections.synchronizedList(List<K>) is a class that can be used to perform regular list operations (add, addAll, get, set, …) in a synchronized manner. Similar methods are also available for other types such as Map, Set, etc.
  • CopyOnWriteArrayList can be used to ensure thread-safe mutating operations on a List (so no ConcurrentModificationException can occur). Any mutating operation on the list will first copy the entire list to a local variable (a snapshot) using an iterator, perform the action and then replace it with the original list. This does not make traversals synchronized.
  • HashTable provides synchronized access to a map. It is noted that only the individual functions of the map are synchronized (so two threads cannot put at the same time and one of them has to wait), so thread-safety on the entire map is not guaranteed.
  • ConcurrentHashMap<K,V> This class can be used to ensure thread-safety on all methods of a regular HashMap. This class has the synchronization abilities of a HashTable, with the addition of being thread-safe on mutating methods. One important note is that retrieving functions (such as get) do not lock the map. Other variants of the Concurrent classes are also available.

Deadlocks

Deadlocks can occur when the first thread locks the first resource and the second thread locks the second resource at the same time and both threads try to lock the other thread’s resource.

Try running the code below (better even, run it locally so you can see the first two prints). This code does not terminate and will run forever because of a deadlock that is happening.

We have two humans, Adam and Eve, that can say hi to each other, and when they do, they make the other person say hi back (talk about relationship issues!😄).

When Adam’s thread gets started, he tries to say hi to Eve, thus locking his own object (with his own @Synchronized) and wait 500ms, meanwhile, Eve’s thread has gotten started and Eve is trying to say hi to Adam and she locks her own object too and just like Adam waits 500ms. After the 500ms, Adam wants to make Eve say hi back to him, and he calls Eve’s sayHiBack, which is also synchronized.

In this case, Adam has to wait for Eve’s thread to finish locking Eve’s object. Same thing happens with Eve, so they both say “hi” and wait on each other forever and never say hi back to each other! (I wish I could end this story on a more happier note!)

When writing synchronized code, we need to think about all the ways objects get locked/unlocked. Deadlocks cannot be detected by the compiler and need to be carefully prevented by good design.

You can create the same deadlock in the example above using other methods such as synchronized statements and reenterant locks.

Conclusion

In this article, multiple methods of synchronization and thread-safety that are being provided by Java and Kotlin, concurrent utilities and deadlocks with were investigated.

I hope you liked the examples as much as I did creating them. There are more classes (like ArrayBlockingQueue) that were left out to keep this article short.

You can also check out my other article on Kotlin Contracts.

Follow me on Medium If you’re interested in more informative and in-depth articles about Java/Kotlin and Android. You can also ping me on Twitter @TheSNAKY 🍻.

https://twitter.com/TheSNAKY/

--

--