Java concurrency is one of the most challenging topics for beginners. After reading many books and blogs, and using it in a production environment, I’ve realized that having a clear learning roadmap for Java concurrency can save a lot of time. In this blog, I’ll highlight the key points of Java concurrency without diving too much into the source code or overwhelming details.

Create a thread

For demonstration purposes, we should know two methods of creating a thread in Java.

The first method is using the constructor of the Thread class directly and implementing the Runnable interface:

  public static void main(String[] args){
      new Thread(() -> {
          System.out.println(Thread.currentThread().getName());
      }, "T1").start();
  }

If a thread needs to return a value or throw an exception, we can implement the Callable interface and use it with FutureTask. This allows us to retrieve the result or handle exceptions via the FutureTask. This is an asynchronous processing approach. For more flexible methods of handling asynchronous tasks, please refer to: CompletableFuture.

  public static void main(String[] args) throws Exception {
      FutureTask<String> futureTask = new FutureTask<>(() -> {
          return Thread.currentThread().getName();
      });
      new Thread(futureTask, "T2").start();
      System.out.println(futureTask.get());
  }

Volatile and JMM

The volatile keyword is a crucial stepping stone in understanding Java concurrency. You'll often see it in the java.util.concurrent package. Its primary function is to ensure memory visibility. But what exactly is memory visibility? To grasp this, we first need to understand the Java Memory Model (JMM). JMM isn’t a physical entity but a concept that helps Java developers understand how memory works in concurrency (and it's different from the JVM).

When two threads access a variable, each typically works with its own local memory. As a result, if thread A modifies a variable, that change might not immediately be visible to thread B, since thread A’s change isn’t flushed to main memory. The volatile keyword solves this problem by forcing the threads to access the variable's value directly from main memory, ensuring that updates are visible across threads.

https://jenkov.com/tutorials/java-concurrency/java-memory-model.html

https://jenkov.com/tutorials/java-concurrency/java-memory-model.html

Race conditions

Race conditions are a common issue in concurrent programming. For example, imagine we want to implement a function that increments a int variable by 1. Now, suppose two threads are performing the same task. Thread A reads the value as 1 and increments it, while at the same time, before local memory flush to main memory, thread B also reads the value as 1 and increments it. The result is that the value may be incremented only once. This example shows why volatile alone cannot guarantee atomicity—it ensures visibility but does not prevent race conditions.

To solve this, we have to prevent multiple thread access same resource. In Java, we can implement it by using synchronized key word or the Lock.

Synchronized

synchronized is designed based on Java objects (specifically the object header), so it can be applied in 4 contexts:

situations code lock scope
instance method `public class Counter {
private int value = 0;
public synchronized void addOne() {
  this.value += 1;

} }| instance object | | static method |public class Counter { private static int count = 0; public synchronized static void addOne(){ value += 1; } }| .class object | | code block(lock instance) |public class Counter { private int value = 0; public void addOne() { synchronized(this) {
this.value += 1; } } }| instance object | | code block(lock .class object) |public class Counter { private static int value = 0; public void addOne() { synchronized(Counter.class) {
this.value += 1; } } }` | .class object |

Lock

We can also explicitly use the Lock interface to manage the timing of lock and unlock operations when dealing with race conditions. The Lock interface provides more flexibility than the synchronized keyword. One common implementation of this interface is the ReentrantLock class.

  public class ReentrantLockLab implements Runnable {

    public static Lock lock = new ReentrantLock();
    public static int value = 0;
    
		  public void run() {
			  lock.lock();
				try {
				    value += 1;
				} finally {
				    lock.unlock();
				}
			}
	}