Multi Thread Programing in Java

Okay Atalay
8 min readOct 18, 2020

Hi everyone. I’ m going to talk about Multi Threading in this article. I explain how thread works and what is going on under the hood. Following topics will be covered.

  1. What is Thread?
  2. What is Multi-Threading
  3. Race Condition
  4. Critical Sections
  5. How to Manage Critical Section
  6. Wait-Notify Mechanism.
  7. How to create Thread
  8. What is Volatile keyword?
  9. What is Daemon Thread
  10. Example: Calculating PI number using Multi-Threads

Let’ s deep dive into topics.

What is Thread?

We can say that Threads are a sub process of the application. Threads are executable by processor. All application must have a main thread at least.
All Threads have own stack and PC (program counter). Threads share the same memory area with the process. So, communication is easy between Threads which work for the same application.

What is Multi Threads?

Application Process may have more than 1 Thread to speed application up. When Processor has multiple core, Threads can work together on the different core in any t-time. Multi Threading applications can work even single core-processor. However, we can say that in any t-time, only 1 thread can work because of hardware limitation. OS chooses which thread(s) should be taken by processor. So, we cannot guarantee that firsly started thread will start to work first.

Process and Threads

Race Condition

Race Condition is a valid term for Multi-Threading applications even if single core processor. When multiple threads try to modify a resource(variable) then resource may be corrupted. We call this corruption as Race-Condition.

Critical Section

Critical section is a code segment which must be executed by a thread instead of multiple threads in any t-time. This code segments must be executed like atomic operation. While critical section is in execution, other Threads must wait until critical section execution is completed.

Multi Thread Overview on Critical Section

Thread-1 takes the lock and executes the critical section(green area). Other Threads are in waited state. When Thread-1 completed critical section codes, one of other threads must be selected to execute critical section. Let’ s choose Thread-2, While Thread-2 is working, Thread-3 and Thread-4 are still in waited state. They are not executing any code. And so on.

4 Threads are working at t1, t5, t6 times.
1 Threads are working at t2 time
2 Threads are working at t3 time
3 Threads are working at t4 time

How to Manage Critical Section

Mutex should be used to allow 1 thread to execute critical section code segment. Mutex means that mutual exclusion. In java, it is easy to use. You do not need to define any Mutex object. All you need to do is to add synchronized keyword to method.

public synchronized void criticalSectionArea() {
// impl your logic here
}

If you want to synchronize a few line of codes in a method, you need to use a lock to do it. When a thread gets the lock, other threads cannot take the same lock. However, other Threads can take another lock(s). Lock can be any Object. Commonly, simple Object is used as a lock.

private static final Object LOCK = new Object();
public void logic() {
// multiple threads can work here
synchronized (LOCK) {
// single thread can work here
}
// multiple threads can work here
}

If we increase an Integer value without synchronization, we can see unexpected results. In the following example, initial value is 0. Then it will be increased 1M times by per thread. When threads are completed, value must be 2M. if we do not use synchronized (LOCK), value may not be 2M. Let’ s have a look following example.

public class Main {
private static final Object LOCK = new Object();
private static int n = 1 * 1000 * 1000;
private static int value = 0;

public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {

@Override
public void run() {
for (int i = 0; i < n; i++) {
synchronized (LOCK) {
value++;
}
}
}
});
Thread thread2 = new Thread(new Runnable() {

@Override
public void run() {
for (int i = 0; i < n; i++) {
synchronized (LOCK) {
value++;
}
}
}
});
// start Threads
thread1.start();
thread2.start();

// wait threads to be completed
thread1.join();
thread2.join();

System.out.println("Value: " + value);
}
}

Wait-Notify Mechanism

Threads should sleep until new task has been assigned not to consume CPU. When a task is created and ready for execution, we need to ping threads to take the task and execute it. Thread which wants to go wait state, must take a lock then must call wait() method of the lock Object. wait() method is part of Object. So all classes have wait() method.

   private static final Object LOCK = new Object();   
// let assume Thread-1 and Thread-2 execute following method
public void dequeue() throws InterruptedException {
synchronized (LOCK) {
if (queue.isEmpty()) {
LOCK.wait();
}
// TODO
}
}

// let assume Thread-3 execute following method
public void enqueue(Object task) throws InterruptedException {
synchronized (LOCK) {
queue.add(task);
LOCK.notifyAll();
}
}

Thread-1 and Thread-2 calls LOCK.wait() method when queue is empty. Then they never continue to run without calling LOCK.notifyAll() method by another thread or sending interrupt by another thread. There are 2 methods to wake threads up. These are notify and notifyAll. notify method wakes a thread up instead of all of them. In above example Thread-1 and Thread-2 are in waiting state. When Thread-3 calls notify method, either Thread-1 or Thread-2 will be woken up. We cannot know which one will be in running state.

Locks must be the same to use this mechanism. If LOCK1 is used instead of LOCK in enqueue method, Thread-1 and Thread-2 cannot continue to execute.

   private static final Object LOCK = new Object();
private static final Object LOCK1 = new Object();
// let assume Thread-1 and Thread-2 execute following method
public void dequeue() throws InterruptedException {
synchronized (LOCK) {
if (queue.isEmpty()) {
LOCK.wait();
}
// TODO
}
}

// let assume Thread-3 execute following method
public void enqueue(Object task) throws InterruptedException {
synchronized (LOCK1) {
queue.add(task);
LOCK1.notifyAll(); // this will not trigger Thread-1 & Thread-2
}
}

Before calling wait, notify and notifyAll methods, the object(lock) must be used in synchronized block. Otherwise, java.lang.IllegalMonitorStateException will be thrown.

public void enqueue(Object task) throws InterruptedException {
synchronized (LOCK) {
queue.add(task);
LOCK1.notifyAll(); // throws IllegalMonitorStateException
}
}

How to Create Thread

There is a few way to create a thread. Firstly, we can extends Thread class then override run method. Secondly, we can implement Runnable interface and give it to thread to call run-method of runnable interface. Let’ s see them

      // 1
Thread thread = new Thread() {
@Override
public void run() {
super.run();
// TODO
}
};
thread.run();
// 2
Thread thread = new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub

}
});
thread.run();
//3
public class Worker extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
}
}

Volatile Keyword

Volatile keyword disables Processor caching capability. Processors have caches to access data fastly. Retrieving data from memory cost is high when ve compare it processor’ s cache. As a default behavior, processors store data in cache even if we change it. Memory data will not be updated immediatelly. If we use the data in multiple threads, Thread-1 may be worked by CPU core-1 and Thread-2 may be worked by CPU core-2. Threads may read dirty data. Let’ s imagine that data was 1 and Thread-1 has changed it to 10 in core-1. Memory is not updated yet. When Thread-2 reads the data, the data will be 1 instead of 10. this causes wrong calculation/operations in the application. To avoid this situation, we have to use volatile keyword. It guarantee to update memory directly bypassing CPU Cache.

private volatile int data;

What is Daemon Thread

Threads can be defined in 2 mode. These are User and Daemon. JVM terminates when all User-Threads completed. JVM never waits Daemon Threads to be terminated. If your thread should not affect JVM termination mechanism, you need to mark it as Daemon.
To mark Daemon, call thread.setDaemon(true), the thread will be marked as Daemon. Otherwise, thread is considered as User-Thread.

JVM starts a few additional Threads besides main Thread. All other threads are daemon. So, application terminates when the main method is completed. Other default threads are GC-threads, Finalizer-thread and etc.

PI number calculation Example

We have learnt all necessary structures to do this example. Here is the PI number formula.

PI Formula

Here is the single thread implementation.

PI calculation impl with Single Thread

Let’ s create Worker class and extends from Thread Class. Then give all necessary parameters to calculate thread local sum. We will use n threads and there will be n times sub-sums. We have to take global-sum by adding all of them to another variable. Then multiply the global-sum by 4.

We need to pass start and end values to worker threads. To retrieve local-sum of threads, localSum array is passed to threads. Each thread will put own localSum result into given index of array. Main thread has to wait for the other threads to complete. join() method of Thread.java is used to do this operation. After all threads are completed, Main Thread will continue automatically and calculate global-sum.

Here is the Worker.java

Worker.java

Here is the Main.java

Main.java

When i run this code with 8 threads, average taken time was around 600 MS. It took around 2500 MS when i set thread count to 1.

Thread count optimum value depends your environment and your implementation. However, it is ideal to set Thread count as to be Processor core count.

--

--