DEV Community

Ahmet Öz
Ahmet Öz

Posted on • Updated on

Thread safety issues in Java

Note: The post content and ideas aggregated from various resources and mainly Brian Goetz's excellent book Java Concurrency in Practice. If you enjoy these notes below, I strongly suggest purchasing the book!

Writing concurrent programs most of the time is primarily related to managing shared mutable data/state between threads.
As threads in the same process sharing the same memory address space (heap) with threads, managing the shared data/state is important to get predictable results from the running application. Basically this makes it hard to create a multithreaded application than a single-threaded application. The penalties for failing to synchronize shared mutable data are liveness and safety failures, so as a developer we need to write a code that synchronizes access of the shared data and which explains the synchronization policy for both clients and maintainers of the code/library.

Atomicity

Operation of a shared state in a concurrent application should be atomic, and if we don't use synchronization mechanisms, concurrent access of operation might cause race conditions.

Race Conditions: A semantic error condition, mostly related to the timing of instruction because thread scheduling algorithms can swap between threads (like context switching) in runtime or other kernel related problems might occur like waiting for some threads on kernel level, so it could cause that instructions on a multithreaded application will not be executed in a sequential manner means that multiple threads can race to access and change the data in the same time, so correctness is based on a timing luck.

Most of the time atomicity problems not easy to predict, and happens mostly on compound actions:

  • read-modify-write
    • for example incrementing a counter, actions: read the value, increment it and write back to memory.
    • to avoid such cases we could use AtomicInteger class in java.util.concurrent.atomic package.
  • check-then-act
    • for example lazy initialization of an expensive object like creating a database connection object.
    • actions: first do a null check if the object is assigned, then create the object.
    • to avoid such cases we could use
      • double-check lock mechanism
      • an enum as enums using lazy initialization by default and all synchronization is handled by JVM.

Visibility

Including the atomicity, we need to make sure that a change in the shared state needs to be visible by other threads because threads using the same memory. In order to ensure the visibility of memory writes across threads, we must use synchronization.

Solutions to adjust visibility between threads:

  • A weaker form of memory visibility using the java volatile keyword. Basically, when an update occurs to a volatile variable the JVM ensures that the change is propagated to memory so threads could see the value change without needing an explicit synchronization. It's weak because does not guarantee atomicity issues. Most of the use cases using volatile is not practical, it's only useful for storing state value of an object in a boolean variable like initialized, stopped ext.
  • A better solution is using the same common locking mechanism for both reading and writing threads because synchronization has no effect unless both read and write operations are synchronized.

Thread safety issues could be solved

  • By using immutable data structures.
  • By not sharing any states between threads. Stateless objects or using pure functions without side effects.
  • By using the java features like implicit locks, explicit locks synchronized keyword, and volatile keyword.
  • By delegating thread safety to well-implemented higher level concurrency utilities in java.util.concurrent package.
  • By not sharing the object reference.
  • By returning defensive copies of a collection instead of returning the mutable collection itself.
  • By using a thread-confinement object that can only be accessed by a current thread-like ThreadLocal.
  • By using encapsulation and locks for securing not thread-safe objects.

Top comments (0)