Saturday, July 17, 2021

Java Tutorial: Multithreading

Chapters

Java Multithreading

Multithreading is the process of executing multiple threads. A thread is a small, lightweight unit of a process that allows to achieve multitasking. Each thread has a separate path of execution, so, each thread is independent to one other. Thus, unless threads don't share resources, execution of one thread doesn't affect the execution of another. So, for example, if one thread encounters an exception, other threads won't be affected by that exception, only the thread that encountered that exception would be affected.

What is a process? A process is often referred to as programs in execution(or sometimes a task) and each process can have multiple threads. The text editor that we are using when writing java codes is an example of process. Process is processed by a processor(CPU). Processor is a broad term that is used as a term for CPU and core.

CPU or central processing unit is a computer processor that is attached to a motherboard. A core is a processor in a CPU that processes task(thread). Task is a unit of work that can be used as a term for a process or a thread.

So, What's the difference between thread and process? Processes are heavyweight whereas threads are lightweight. A process is comprised of multiple threads whereas a thread is a subset of process. processes have its own address space in memory whereas threads use their process' address space and share it with one another.

Don't be confuse between concurrency and multithreading. Concurrency involves parallelism whereas thread is the use of multiple threads. Operating systems schedule threads in every processor. This scheduling is called context switch.

Parallelism consists of two threads that run in parallel(at the same time). To achieve parallelism, we very likely need to have two or more processors. A single processor can handle multiple threads. In a single processor that handles multiple threads, context switching is implemented.

Context switch gives every thread a slice of time to run until all of their tasks are complete or they're all dead. Context switch gives an illusion of parallelism. That's why a computer that only has a single processor can still do multiple tasks.

Creating Thread

There are two ways to create a thread: by extending the Thread class or implementing the Runnable interface. Each method has its own merits. Once we create a thread, we can start it by invoking the start() method of the Thread class.

Implementing Runnable Interface

If we want to create a simple thread then, we just need to create a class that implements a Runnable interface and overrides run() method.
public class SampleClass{

  public static void main(String[]args){
    System.out.println("Main Thread Started!");
    Thread t = new Thread(new MyRunnable());
    t.start();
    System.out.println("Ending Main Thread...");
  }
}

class MyRunnable implements Runnable{

  @Override
  public void run(){
    System.out.println("Thread Started!");
    for(int i = 0; i < 5;i++)
      System.out.println(i);
  }
}

Note: Your result might be different from mine
'cause thread process is related to CPU
performance. Especially, if two or more threads
are running simultaneously

Result
Main Thread Started?
Ending Main Thread...
Thread Started!
0
1
2
3
4
First off, We created a class called MyRunnable that implements Runnable interface. Then, we overrode run() method in Runnable interface. Threads execute codes in the run() method once they start. Then, we instantiated a Thread class and put MyRunnable in the constructor as an argument. Then, we started that thread and it executed the codes that we put in the run() method.

Next, notice the result. So, we have two threads running in the example: Main thread and thread t. Main thread is the thread that automatically starts everytime JVM calls the main() method. We can use the main thread to start other threads.

Also, we can see in the result that, the main thread ended first before thread t started. Threads won't be terminated if one of the active threads is terminated.

We can also use anonymous class as a Runnable. If you're using java8 and above then, we can use lambda expression as an alternative to anonymous class since runnable has only one abstract method.
public class SampleClass{

  public static void main(String[]args){
    System.out.println("Main Thread Started!");
    
    //Creating a runnable using lambda
    Thread t = new Thread(() ->{
      System.out.println("Thread Started!");
      for(int i = 0; i < 5;i++)
      System.out.println(i);
    });
    t.start();
    
    System.out.println("Ending Main Thread...");
  }
}

Note: Result may vary
Result
Main Thread Started?
Ending Main Thread...
Thread Started!
0
1
2
3
4
Extending Thread Class

We can create a thread by extending the Thread class. If we want to take advantage of some members of the Thread class then extending the Thread class is a good choice.
public class SampleClass{

  public static void main(String[]args){
    
    Counter myCounter = new Counter(
                        new int[]{1,2,3,4,5});
    
    myCounter.start();
  }
}

class Counter extends Thread{
  int[] numbers;
  
  Counter(int[] numbers){
    this.numbers = numbers;
  }
  
  @Override
  public void run(){
    for(int i = 0; i < numbers.length;i++)
      System.out.print(numbers[i] + " ");
  }
  
  @Override
  public void start(){
    //don't forget to call the start()
    //of thread class if you override
    //the start() method. Otherwise,
    //your thread won't start.
    if(numbers != null)
      super.start();
    else
      System.out.println("Can't start thread "+
                        "'cause the array is null!");
  }
}

Result
1 2 3 4 5
sleep() Method

sleep() is a static method that causes a thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers. sleep() throws InterruptedException which is a checked exception. Thus, we need to handle it.
public class SampleClass{

  public static void main(String[]args){
    Thread t = new Thread(() ->{
      
       for(int i = 0; i < 5;i++){
         System.out.print(i + " ");
         try{
           Thread.sleep(100);
         }
         catch(InterruptedException e){
           System.out.println("Thread has been"
                             +" interupted!");
           e.printStackTrace();
         }
         
       }
       
    });
  }
}
The example above displays numbers per 100 milliseonds interval. sleep() has another overloaded form: sleep(long millis,int nanos)
We can use that form if we want a time with nano-precision.

currentThread() Method

This static method returns a reference to the currently executing thread object.
public class SampleClass{

  public static void main(String[]args){
    Thread mainThreadName = Thread.currentThread();
    System.out.println("Executing thread name: " + 
                       mainThreadName.getName());
                       
    Thread t = new Thread(() ->{
      System.out.println("Executing thread name: " + 
                       Thread.currentThread().getName());
    },"MyThread");
    t.start();
    
  }
}

Result

Executing thread name: main
Executing thread name: MyThread
Stopping Thread By Using Boolean Flag

We can use a boolean flag to kill/stop a thread. We can't use the stop() method in Thread class because that's obsolete.
public class SampleClass{

  public static void main(String[]args){
  
    Counter myCounter = new Counter();
    try{
      myCounter.start();
      Thread.sleep(500);
      myCounter.stopThread();
    }catch(InterruptedException e){
      e.printStackTrace();
    }
    
  }
}

class Counter extends Thread{
  private boolean stopFlag = false;
  private int count = 0;
  
  @Override
  public void run(){
    while(!stopFlag){
      try{
        System.out.println("count: " + count);
        count += 1;
        sleep(100);
      }
      catch(InterruptedException e){
        e.printStackTrace();
      }
    }
    System.out.println("Thread ended!");
  }
  
  public void stopThread(){
    stopFlag = true;
  }
  
}

Result
count: 0
count: 1
count: 2
count: 3
count: 4
Thread Ended!
Stopping Thread By Interruption

We can interrupt a thread in order to stop its execution. Unlike in boolean flag method, we need a java element that throws InterruptedException like the sleep() method.
public class SampleClass{

  public static void main(String[]args){
  
    Counter myCounter = new Counter();
    try{
      myCounter.start();
      Thread.sleep(500);
      
      //interrupt() method
      //interrupts the thread
      //that invokes interrupt()
      myCounter.interrupt();
    }catch(InterruptedException e){
      e.printStackTrace();
    }
    
  }
}

class Counter extends Thread{
  private int count = 0;
  
  @Override
  public void run(){
    try{
      while(true){
        System.out.println("count: " + count);
        count += 1;
        sleep(100);
      }
    }
    catch(InterruptedException e){
      System.out.println("Thread has been"+
                         " interrupted!");
    }
  }
  
}

Result
count: 0
count: 1
count: 2
count: 3
count: 4
Thread has been interrupted!
We can use the interrupted() or isInterrupted() method if we want to check if a thread is interrupted or not. The differences between interrupted() and isInterrupted() are: interrupted() is static whereas isInterrupted() is non-static; interrupted() clears the interrupt status of a thread once it's invoked whereas isInterrupted() doesn't clear the interrupt status of a thread once it's invoked.

Interrupt status is a flag in every thread that indicates if a thread is interrupted or not. interrupt() method sets the interrupt status of threads.
public class SampleClass{

  public static void main(String[]args){
  
    Counter myCounter = new Counter();
    try{
      myCounter.start();
      Thread.sleep(200);
      myCounter.interrupt();
    }catch(InterruptedException e){
      e.printStackTrace();
    }
    
  }
}

class Counter extends Thread{
  private int count = 0;
  
  @Override
  public void run(){
    try{
      while(!isInterrupted()){
        System.out.println("count: " + count);
        count += 1;
        sleep(5000);
      }
      throw new InterruptedException();
    }
    catch(InterruptedException e){
      System.out.println("Thread has"
                +" been interrupted!");
    }
  }
}

Result
count: 0
Thread has been interrupted!
One of the advantages of interruption is that, interruption can stop the thread even it's blocked. Unlike using a boolean flag. Take a look at this example.
public class SampleClass{

  public static void main(String[]args){
  
    Counter myCounter = new Counter();
    try{
      myCounter.start();
      Thread.sleep(200);
      myCounter.stopThread();
    }catch(InterruptedException e){
      e.printStackTrace();
    }
    
  }
}

class Counter extends Thread{
  private boolean stopFlag = false;
  private int count = 0;
  
  @Override
  public void run(){
    while(!stopFlag){
      try{
        System.out.println("count: " + count);
        count += 1;
        sleep(5000);
      }
      catch(InterruptedException e){
        e.printStackTrace();
      }
    }
    System.out.println("Thread ended!");
  }
  
  public void stopThread(){
    stopFlag = true;
  }
  
}

Result
count: 0
Thread ended!
This example couldn't stop the thread when it was blocked. We needed to wait for the thread to be unblocked in order to check the boolean flag.

Though, in my personnal preference, I still prefer using boolean flag when I wanna stop threads. For me, using boolean flag is much simplier than interruption. Though, there are circumstances where interruption is more preferrable than boolean flag.

join() Method

join() waits the thread that invokes this method to die. join() ensures that any instructions next to this method won't be executed unless the thread that calls this method dies or the time specified in this method is due. join() throws InterruptedException which is a checked exception. Thus, we need to handle it.
public class SampleClass{

  public static void main(String[]args){
    System.out.println("Main Thread Started!");
    Counter c1 = new Counter("Counter1");
    Counter c2 = new Counter("Counter2");
    Counter c3 = new Counter("Counter3");
    try{
      c1.start();
      
      //this join() is equivalent to join(0)
      c1.join();
      //code below won't be executed unless
      //c1 dies
      
      System.out.println();
      c2.start();
      c2.join(500);
      //code below won't be executed unless
      //the time in millis that is specified
      //is due
      
      System.out.println();
      c3.start();
      c3.join(500,500_000);
      //code below won't be executed unless
      //the time in millis + nanos that
      //is specified is due
      
      System.out.println("Main Thread Ended!");
    }catch(InterruptedException e){
      e.printStackTrace();
    }
    
  }
}

class Counter extends Thread{
  private String name;
  
  Counter(String name){
    this.name = name;
  }
  
  @Override
  public void run(){
  
    try{
      for(int i = 0; i < 5;i++){
        System.out.println(name + " " + i);
        sleep(100);
      }
    }
    catch(InterruptedException e){
      e.printStackTrace();
    }
    
  }
}

Result

Main Thread Started!
Counter1 0
Counter1 1
Counter1 2
Counter1 3
Counter1 4

Counter2 0
Counter2 1
Counter2 2
Counter2 3
Counter2 4

Counter3 0
Counter3 1
Counter3 2
Counter3 3
Counter3 4
Main Thread Ended!
In the example above, we have four threads, the main thread an three user threads. Once main thread calls c1.join(), main thread will temporarily stop its execution while waiting for c1 to die. Then, After c1 dies, main thread continues and calls c2.join().

c2.join() has specified time so, main thread will stop executing for the specified time. Once the specified time is due, main continues its execution and calls c3.join(). c3.join() also has specified time so, main thread will stop executing for the specified time. Once the specified time is due, main continues its execution and eventually dies.

join() dependent on OS for timing. So, don't assume that join() will exactly stop for a specified time that we put in it.
Lifecycle Of a Thread

Threads go through different states during their lifetime. Basically, threads have four states: New, Runnable, Blocked or Non-Runnable and Terminated.

Thread Lifecycle
New: This is the state of every thread that is newly created but they aren't started yet. Their states change if they start to run.
Runnable: This state indicates that a thread is running or is being prepped to run.
Blocked: This state indicates that a thread is not running temporarily or indefinitely.
Terminated: This state indicates that a thread is terminated or in other words, the thread is dead.

Thread States

In java, thread states are wrapped into an enum class. To get the state of a thread, we will use the getState() method in Thread class. These are the thread states accoding to Enum Thread.State.

NEW: A thread that has not yet started is in this state.

RUNNABLE: A thread executing in the Java virtual machine is in this state.

BLOCKED: A thread that is blocked waiting for a monitor lock is in this state.

WAITING: A thread that is waiting indefinitely for another thread to perform a particular action is in this state.

TIMED_WAITING: A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.

TERMINATED: A thread that has exited is in this state.
public class SampleClass{

  public static void main(String[]args){
    System.out.println("Main Thread Started!");
    
    //we use getState() method to get the state
    //of a thread
    System.out.println("Main Thread state: " +
                       Thread.currentThread().
                       getState());
                       
    Counter c1 = new Counter("Counter1",
                             Thread.currentThread());
    System.out.println("c1 has been created!");
    System.out.println("c1 Thread State: "+
                         c1.getState());
    try{
      c1.start();
      c1.join();
      
      System.out.println("c1 Thread State: "+
                          c1.getState());
                         
      System.out.println("Main Thread Ended!");
    }catch(InterruptedException e){
      e.printStackTrace();
    }
    
  }
}

class Counter extends Thread{
  private String name;
  Thread mainThread;
  
  Counter(String name,Thread mainThread){
    this.name = name;
    this.mainThread = mainThread;
  }
  
  @Override
  public void run(){
    System.out.println("c1 has been started!");
    System.out.println("c1 Thread State: "+
                       getState());
    
    try{
      for(int i = 0; i < 5;i++){
        System.out.println(name + " " + i);
        sleep(100);
      }
      System.out.println("Main Thread State: "+
                         mainThread.getState());
    }
    catch(InterruptedException e){
      e.printStackTrace();
    }
    
  }
}

Result

Main Thread Started!
Main Thread State: RUNNABLE
c1 has been created!
c1 Thread State: NEW
c1 has been started!
c1 Thread State: RUNNABLE
Counter1 0
Counter1 1
Counter1 2
Counter1 3
Counter1 4
Main Thread State: WAITING
c1 Thread State: TERMINATED
Main Thread Ended!

Thread Priorities

Thread priorities are ranked from 1 to 10. 10 is the highest priority whereas 1 is the lowest priority. Threads with higher priorities preempt threads with lower priorities. In case two threads that have the same priority, a FCFS(First Come First Serve) ordering is followed. Also, high priority threads get more CPU time. In java, there are three static constants that represent thread priorirites: MAX_PRIORITY(10), NORM_PRIORITY(5) and MIN_PRIORITY(1).

If we don't specify the priority of a thread then, JVM will assign the default priority value which is NORM_PRIORITY. Thread scheduler handles the schedules of threads.

Note: High priority threads don't guarantee to be executed first over lower priority threads even we assume that they're scheduled first in the scheduler. The OS scheduler is still the one that will decide when and where a thread should run. The OS may follow the JVM's scheduler or not.
public class SampleClass{

  public static void main(String[]args){
    Thread t1 = new Thread();
    Thread t2 = new Thread();
    Thread t3 = new Thread();
    
    //use setPriority to set thread priority
    //we can use int literal or the three
    //constants
    t1.setPriority(Thread.MAX_PRIORITY);
    t2.setPriority(6);
    t3.setPriority(Thread.MIN_PRIORITY);
    
    //use getPriority() to get thread priority
    System.out.println("t1 priority: " + t1.getPriority());
    System.out.println("t2 priority: " + t2.getPriority());
    System.out.println("t3 priority: " + t3.getPriority());
  }
}

Thread Scheduler

JVM has thread scheduler that handles the schedules of java application threads. Scheduling schedules threads in some order. There are scheduling algorithms that are implemented in JVMs and we need to have a basic understanding of those algorithm in order to have an idea of how JVMs schedule threads.

Preemptive scheduling schedules threads based on their priorities. Threads with higher priorities can preempt threads with lower priorities. For example, a lower priority thread may pause if the allocated time or time slice to that thread is due or a higher priority thread comes back online and preempts the lower priority thread. Depending on JVM implementation, JVM usually uses Round Robin scheduling which is a type of preemptive scheduling.

Note: High priority threads don't guarantee to be executed first over lower priority threads even we assume that they're scheduled first in the scheduler. The OS scheduler is still the one that will decide when and where a thread should run. The OS may follow the JVM's scheduler or not.

First Come First Serve Scheduling schedules based on their order. The first thread that requests for CPU time will be the first one to get CPU time. This algorithm is managed by FIFO(First In First Out) queue. FCFS is a non-preemptive scheduling algorithm.

Time Slicing Scheduling heavily relies on time slice or time quantum. This preemptive algorithm divides CPU time to multiple time slices or quanta and then allocate those quanta into threads that are in ready state. This algorithm executes threads in a cyclical manner. Time Slicing Scheduling is also called Round Robin Scheduling.

Java Synchronization

Synchronization is a process of controlling threads that access any shared resource. If we want one thread to access any shared resource at a time then, we can use synchronization. In java, we can use synchronized methods or statements/blocks to achieve synchronization. Synchronization using synchronized methods or blocks is called mutual exclusion.

Mutual exclusion ensures that only one thread can access a critical section at a time. Critical section is a part of a code that alters memory value that can cause conflicts between threads. For example, threadA assigns value to variableA while threadB reads the data of variableA.

This situation is called a critical section 'cause threadA is altering the value of variableA while threadB is accessing the value of variableA. ThreadB might not be aware of the changes of variableA in threadA. Thus, there might be a conflict between threadA and threadB.

Any synchronized elements like volatile fields and codes in synchronized methods/blocks are considered as thread safe. Thread safe is a term that is used to indicate that code elements like fields, statements, etc. are safe from inconsistent data due to multiple threads updating those elements.

Thread Interference

Thread interference or interleaving happens when two operations run in different threads and accessing the same resource. These operations are comprised of multiple steps and these steps may overlap one another.

For example, we have a class and that class is accessed by two or more threads.
class Counter{
  private int count;
  
  public void increase(){
    count++;
  }
  
  public void decrease(){
    count--;
  }
}
By looking at the class above, it seems like thread interference is not going to be a problem 'cause we're only using the posfixes "++" and "--". However, there are single operations that can be broken down into multiple steps and increment(++) and decrement(--) operators are some of those.

For example, increment(++) can be decomposed into three steps:
1.) Gets the current value of count
2.) Increases the value by 1
3.) Assigns the modified value to count

The steps are the same as the steps of decrement(--) except for the second step. Change the second step from "increase" to "decrease".

Suppose thread A calls increase() and at the same time thread B calls decrease() and we expect count to become zero. This situation may lead to thread interferece. Suppose a thread interference happens between thread A and thread B. If so, the overlaps might be like this:
Thread A gets the current value of count
Thread B gets the current value of count
Thread B decreases the value of count by 1. The value is now -1
Thread B increases the value of count to 1. The value is now 0
Thread A assigns 0 to count. Now count is 0.
Thread B assigns -1 to count. Now count is -1.

Due to thread interference, count value became -1 instead of 0, which is not the result that we expected. Thread interference can be very problematic especially in a complex concurrency structure. Synchronization can help us to avoid thread interference.

Memory Consistency Errors

Memory Consistency Errors happen if two or more threads access the same resource but the value of the resource is inconsistent with each thread. The causes of these errors are complex and not going to be covered in this tutorial.

Knowing the causes of consistency errors is not required when creating a strategy to avoid these errors. However, we need to understand an important concept in multithreading, which is the happens-before relationship.

Happens-before relationship guarantees that the changes to shared resource in one thread is visibile to another thread that is using the resource.

For example, we have variable named as "counter" with zero initial value and then, threadA increases the value of counter by 1. Then threadB calls println() to print the value of counter on the console. If this scenario happens in a single thread then, we can assume that the value that is going to be displayed is 1.

If the increment code is executed by a thread and at the same time, the println() is called by another thread then, consistency errors may happen. Since we're dealing with two executions, we don't know if threadB is aware of the changes that happens in threadA.

The resolve this problem, we need to establish a happens-before relationship between threadA and threadB. Synchronization using synchronized blocks or methods is one of the features of java that can establish happens-before relationship between threads. We will learn about synchronized methods and blocks later.

We already witness some happens-before scenarios in previous topics. These scenarios create happens-before relationship.
  • If one thread executes statements before it creates a new thread then, every changes those statements made are visible to the newly created thread. For example, We have a variable named as "counter" with 0 initial value and then, threadA increments its value by 1 and after that, we create and start threadB. Now, threadB displays the value of counter.

    In this scenario, it's safe to assume that the value that is going to be displayed is "1", since the increment only happened in the individual thread and then that thread subsequently created threadB after the increment.

  • If a thread is terminated due to the invocation of join() method then, the changes that happened in the terminated thread is visible to the thread that invoked the join() method.
Intrinsic Locks

Intrinsic locks or monitor locks(Java API calls these lock as "monitors") enforce exclusive access to an object's state and building happens-before relationship between threads. Every object has intrinsic lock and a thread needs to acquire that lock first in order to have an exclusive access to that object. Once the thread is done accessing the object, it can release the lock so other threads have a chance to grab the lock.

As long as a thread holds an object's lock, other threads can't acquire that lock unless the thread releases the lock. In java synchronization, threads can't acquire the same lock that is owned by another thread.

When a lock is released, a happens-before relationship is created between the changes that happened in the former thread that owned the lock and the other threads that might acquire the lock.

These mechanics are part of java synchronization and we can implement these mechanics by using synchronized methods or statements/blocks.

Synchronized Method

A thread acquires an object's lock if the thread invokes one of the object's synchronized methods. The thread releases the lock once the method returns the execution to the caller which is the thread itself. The lock is released even if the return statement throws an exception.
public class SampleClass{

  public static void main(String[]args){
    Counter c1 = new Counter();
    
    Thread t1 = new Thread(new MyRunnable(c1,"t1","increase"),
                           "t1");
    Thread t2 = new Thread(new MyRunnable(c1,"t2","increase"),
                           "t2");
    Thread t3 = new Thread(new MyRunnable(c1,"t3","decrease"),
                           "t3");
    Thread t4 = new Thread(new MyRunnable(c1,"t4","increase"),
                           "t4");
    
    t1.start();
    t2.start();
    t3.start();
    t4.start();
  }
}

class MyRunnable implements Runnable{
  private Counter counter;
  private String name;
  private String operation;
  
  MyRunnable(Counter counter, String name,
             String operation){
    this.counter = counter;
    this.name = name;
    this.operation = operation;
  }
  
  @Override
  public void run(){
    counter.count(name,operation);
  }
  
}

class Counter{
  private int num = 0;
  
  //To create a synchronized method
  //use the synchronized keyword
  synchronized void count(String threadName,
                          String operation){
    System.out.print("Thread: " + threadName);
    if(operation.equals("increase"))
      System.out.print(" num: " + ++num);
    else if(operation.equals("decrease"))
      System.out.print(" num: " + --num);
    System.out.println();
  }
  
}

Note: Result may vary
Result
Thread: t1 num: 1
Thread: t3 num: 0
Thread: t2 num: 1
Thread: t4 num: 2
By using synchronized method, we can avoid thread interference and memory consistency errors. Remember, a happens-before relationship is established when a thread releases a lock. We can create a static synchronized method. The lock of synchronized method resides at the class itself not in the instances of the class. Note, Synchronized constructor is not allowed.

Synchronized Block/Statement

Another way of synchronizing java codes is to use synchronized block. This block synchronizes a collection of statements that may be related to the specified object. The specified object is in the parentheses of the block. Take a look at this code excerpt.
public class Counter{
  private int num1 = 0;
  private final int num2;
  ArrayList<Integer> numList;
  
  Counter(int num2,ArrayList<Integer> numList){
    this.num2 = num2;
    this.numList = numList;
  }
  
  void count(){
    //final primitive variables can be 
    //safely read in non-synchronized 
    //methods/blocks 'cause these
    //variables are constants. Thus,
    //their values won't change
    System.out.println("num2: " + num2);
    
    //This is how we create a synchronized
    //block. The "this" in the parentheses
    //is the object resource where its lock
    //is gonna be acquired by threads that
    //are gonna access this block
    synchronized(this){
      num1 += num2;
      System.out.println("num1: " + num1);
      
      //If possible, avoid invoking other
      //objects' methods from synchronized
      //methods/blocks doing so may lead to
      //nasty problems like deadlock, 
      //starvation, etc.
      //numList.add(num1);
    }
    
    //It's preferrable to put other objects'
    //methods out of synchronized block/method
    numList.add(num1);
  }
}
Sometimes, we want some variables that have nothing to do with one another to be synchronized separately. We can use a dummy objects,if you will, to synchronize those variables. Take a look at this code excerpt.
public class Counter{
  private int num1;
  private int num2;
  //Dummy objects. The sole purpose
  //of dummy objects is to act as a lock.
  //These objects are unrelated to
  //num1 and num2
  Object o1 = new Object();
  Object o2 = new Object();
  
  void num1Inc(){
    synchronized(o1){
      num1++;
    }
  }
  
  void num2Inc(){
    synchronized(o2){
      num2++;
    }
  }
  
}
Now, any threads that want to increment num1 can access num1Inc() method. To increment num2, threads have to access num2Inc() method. In this way, any unnecessary blocks between num1 and num2 are removed.

Imagine putting num1 and num2 in one synchronized method, more threads have to access that method synchronously. Therefore, more threads have to be in waiting state.

Tip: String objects are not good candidate for synchronized blocks because some of them may reside in a special memory pool called String Constant Pool. Locking an object inside SCP may degrade our app performance because String object is one of the most used objects in Java. Therefore, SCP is one of the most accessed memory area in a java application. Instead, we can use Object instance as an alternative.

Multiple Locks Acquisition

A thread can acquire multiple locks as long as those locks are not owned by other threads. Though, Acquiring multiple locks may cause nasty problems like deadlock.
class ClassA{

  synchronized void meth1(){
  }
}

class ClassB{
  ClassA a1= new ClassA();
  
  synchronized void meth2(){
    a1.meth1();
  }
}
So, for example, ThreadA access ClassB object and invoke meth2(). When ThreadA invokes meth2(), that thread acquires the lock of ClassB's object. Then, meth1() is invoked by ThreadA so, the thread acquires the lock of ClassA's object. Now, ThreadA has two locks.

Reentrant Synchronization

A thread can acquire a lock that it already owns more than once. This process is called reentrant synchronization. Take a look at this code.
public class ClassA{

  synchronized void meth1(){
  }
  
  void meth2(){
    synchronized(this){
      meth1();
    }
  }
}
So, let's say threadA invokes meth2() and then it enters in the the synchronized block. Thus, threadA acquires the lock of ClassA's object. Then, threadA invokes meth1 and subsequently acquire the lock of ClassA's object again. So, threadA has two locks of ClassA's object.

Now that we have two locks, threadA needs to release two locks in order to fully release the lock of ClassA's object. Otherwise, other threads can't acquire the lock of ClassA's object.

Imagine if there's no reentrant synchronization. ThreadA can't invoke meth1() 'cause the lock of ClassA's object is already owned by threadA itself. So, to invoke meth1(), threadA needs to release the lock it's owning. To release the lock, threadA needs to exit the synchronized block first.

ThreadA can't leave the synchronized block 'cause it needs to invoke meth1() and meth1() requires threadA to release its lock. Therefore, threadA is blocking itself.

Inter-Thread Communication

The process of communication between threads in synchronized environment is called Inter-thread Communication or Co-operation. We already know that threads communicate with other threads through acquiring object locks in synchronized environment. We can improve the communication between threads by using the three methods in the Object class: wait(), notify() and notifyAll().

wait(), notify() and notifyAll() Methods

wait() method temporarily stops the execution of a thread that invoked the method and releases the lock that the thread holds. The thread state is changed to a waiting state and will wait for an opportunity to acquire the object's lock again once notified.
notify() method awakens one thread that is waiting on a particular object's lock. If two or more threads are waiting for the object's lock, one of them will be awakened arbitrarily. The arbitrary choice depends on the implementation.
notifyAll() method awakens all threads that are waiting on a particular object's lock.

Note: notify() and notifyAll() don't give up locks to awakened threads. Their job is only to awaken threads.
Note: These methods can only be used in synchronized environment like synchronized methods and blocks.

Let's take a look at this simple cosumer-producer example.
public class SampleClass{

  public static void main(String[]args){
    Counter c1 = new Counter();
    
    //ThreadA
    new Thread(() ->{
      c1.consume();
    }).start();
    
    //ThreadB
    new Thread(() ->{
      c1.produce();
    }).start();
    
  }
}

class Counter{
  private int num = 1;
  private boolean isProduced = false;
  
  void consume(){
    synchronized(this){
      try{
        if(!isProduced)
          wait();
      }
      catch(InterruptedException e){
        e.printStackTrace();
      }
      System.out.println("Consumed number: " + num);
      notify();
    }
  }
  
  void produce(){
    synchronized(this){
      try{
        if(isProduced)
          wait();
      }
      catch(InterruptedException e){
        e.printStackTrace();
      }
    
      num += num;
      System.out.println("Produced number: " + num);
      notify();
    }
  }
}

Result
Produced number: 2
Consumed number: 2
So, threadA starts first and invokes consume(). Though, it invokes wait() and invoking wait() changes threadA state from running to waiting state. Then, threadB starts and invokes produce(). Then, the produced number is displayed on the console and threadB invokes notify() to notify waiting threads and invokes wait() to pause itself. notify() notifies one of the threads that is waiting for the lock of Counter object.

In this example, threadA is the only waiting thread before threadB invoked notify(). So, threadA is notified and continues its execution from where it left. Then, the consumed number is displayed on the console and threadA invokes notify() to notify threadB.

Deadlock

Deadlock is a situation where a thread is forever waiting for another thread to release a lock or two threads are waiting for the locks of each other. For example, threadA is waiting for objectB lock that is owned by threadB. ThreadB is waiting for objectA lock that is owned by threadA. Let's take a look at this example.

Note: This example will be non-responsive. So, we need to terminate this program by force. If you're on windows and using cmd, just press CTRL+C to terminate the program.
public class SampleClass{

  public static void main(String[]args){
    Object o1 = new Object();
    Object o2 = new Object();
    
    new Thread(()->{
      
      synchronized(o1){
        System.out.println("ThreadA acquires o1 lock!");
        try{Thread.sleep(150);}
        catch(InterruptedException e){
          e.printStackTrace();
        }
        synchronized(o2){
          System.out.print("ThreadA acquires o2 lock!");
          System.out.println();
        }
      }
    }).start();
    
    new Thread(() ->{
        synchronized(o2){
        System.out.println("ThreadB acquires o2 lock!");
        try{Thread.sleep(150);}
        catch(InterruptedException e){
          e.printStackTrace();
        }
        synchronized(o1){
          System.out.print("ThreadB acquires o1 lock!");
          System.out.println();
        }
      }
    }).start();
    
  }
}

Result
ThreadA acquires o1 lock!
ThreadB acquires o2 lock!
First off, threadA has been started and acquired o1 lock. Then, threadB has been started and acquired o2 lock. After that, threadA couldn't acquire o2 lock because it was owned by threadB. ThreadB couldn't acquire o1 lock because it was owned by threadA. Therefore, the two threads are in a deadlock situation.

Deadlock is unfixable once it occurs. The only way to fix deadlock is to redesign our thread structure.

Contention, Starvation and Livelock

Contention, Starvation and livelock are less common problems than deadlock. Though, It's better to learn them 'cause, once we design a concurrent software, we may encounter them in the future and it's our responsibility to fix them.

Contention is a situation where a greedy thread or high priority thread, holds a shared resource for too long indefinitely blocking other threads in the process. Contention creates an imbalance between threads and that imbalance may hurt our software performance.

Starvation is a situation where a thread can't access a shared resource because other threads access it first. This situation leaves the thread "starved".

Livelock is a situation where two threads are busy responding to each other's actions. Thus, stalling the progress of both threads. Unlike deadlock, threads in livelock are not blocked, they're just too busy responding to each other.

Livelock is comparable to a scenario where two people want the other to eat first. For example, personA wants personB to eat first. At the same time, personB wants personA to eat first. These people are trying their best to respond to each other's request and both of them are not backing off. Thus, both of them are not eating and they're convincing each other to eat first forever.

volatile Keyword

volatile keyword can be used in global fields to ensure memory visibility just like what other synchronization methods do. Just like other synchonization methods, this keyword solves some problems regarding Cache Coherence.

We can use this keyword to implement non-blocking synchronization. However, volatile keyword doesn't support mutual exclusion. Because of this, thread interference(race condition) may occur. This is how we apply volatile keyword to fields.
public class SampleClass{
  
  private static volatile int num = 0;
  //or
  //static private volatile int num = 0;
  
  public static void main(String[]args){
    
    new Thread(()->{
    System.out.println(Thread.currentThread().getName()+
	": " + (++num));
    }).start();
    
  }
}

Result(may vary)
Thread-0: 1
Thread-1: 2
Note that thread interference may occur in the example above. These are the benefits that we're going to get if we use the volatile keyword:
  • Memory Visibility
  • Happens-Before Relationship
One of the advantages of this keyword is that we can update variables via atomic operations with the effect of Happens-Before Relationship. I explained atomic operations in this article.

Atomic operations with Happens-Before Relationship avoid data race. In terms of performance, non-blocking synchronization like volatile keyword is less expensive than blocking synchronization like synchronized keyword.

Reordering

Reordering is a way of improving program's performance by allowing Java or CPU to rearrange instructions. For example, threadA has these statements:
Statements #1
int i1 = 9;
int i2 = i1;
i1++;

Java or the CPU may rearrange the statements above like this:
Statements #2
int i1 = 9;
i1++;
int i2 = i1;

Let's assume that the statements above are not synchronized. As we can see, i2 in "statements #1" example is 9 and i2 in "statements #2" is 10. This reordering mechanism may also happen in synchronized environment. Although, reordering volatile fields and synchronized statements is sequentially consistent and respects the happens-before relationship.

According to jls-17.4.5: "It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal."

Roach Motel Principle

Reference: JMM Interpretation: Roach Motel
Java or CPU may move codes into a synchronized block. Once the codes move into the synchronized block, they can't be move out of the block. This process is called the Roach Motel Principle. Take a look at this example.
Original Order
a++;
synchronized(object){
  int b = 3;
}
c = 2;

Possible Reorder #1

synchronized(object){
  a++;
  int b = 3;
}
c = 2;

Possible Reorder #2

a++;
synchronized(object){
  int b = 3;
  c = 2;
}
As we can see, our codes may be reordered in diferent ways. This type of reordering is allowable as long as it doesn't violate the Java Memory Model.

Daemon Thread

Daemon thread is a thread that is suitable for background tasks. One example of daemon thread is the garbage collector. Garbage collector is a daemon thread that manages memory allocation of java programs.

There are two types of threads in java: User thread or non-daemon thread; and daemon thread. JVM prioritizes users threads more than daemon threads. When all user threads die, JVM terminates the program and kill all deamon threads. Unlike user threads, JVM won't kill any user thread or terminate itself if all deamon threads die.

Daemon thread is suitable for background tasks and it's recommended to be easily terminated. Blocking mechanism like join() in daemon thread may block the termination of a program even all user thraeds are dead.

To create a daemon thread, we need to create a user thread first and set the thread to a daemon thread.
public class SampleClass{

  public static void main(String[]args){
    Thread t1 = new Thread(() -> {
       while(true){}
    });
    
    //use isDaemon() method to check if a
    //thread is a deamon thread
    System.out.println(t1.isDaemon());
    
    //use setDaemon() method to set a thread
    //to a daemon thread
    t1.setDaemon(true);
    System.out.println(t1.isDaemon());
    t1.start();
    System.out.println("Main Thread End!");
  }
}
In the example above, t1 is terminated even it runs an infinite loop. Once the main thread is terminated, JVM terminates all daemon threads and subsequently end the program.

ThreadGroup Class

We can use the ThreadGroup class to group threads and thread groups so, we can easily manage many threads of our program. Thread groups form a tree where every thread group, except for the initial thread group, has a parent.
public class SampleClass{

  public static void main(String[]args){
    //ThreadGroup constructor with ThreadGroup name
    //as argument
    ThreadGroup tg1 = new ThreadGroup("Group1");
    //ThreadGroup constructor with parent thread group
    //and thread group name as arguments
    ThreadGroup tg2 = new ThreadGroup(tg1,"Group2");
    
    //create threads and assign groups
    new Thread(tg1,"t1");
    new Thread(tg1,"t2");
    new Thread(tg2,"t3");
    new Thread(tg2,"t4");
    new Thread(tg2,"t5");
    
    //activeCount() returns an estimate of the number
    //of active threads in this thread group and its
    //subgroups.
    System.out.println(tg1.getName() + " active threads: "
                       + tg1.activeCount());
    
    //activeGroupCount() returns an estimate of the
    //number of active groups in this thread group and
    //its subgroups.
    //
    System.out.println(tg1.getName() + " active groups: "
                       + tg1.activeGroupCount());
    
    //getParent() returns the parent of this thread
    //group.
    //Group1 parent is the main thread group which is the
    //initial thread group
    System.out.println(tg1.getName() + " parent: "
                       + tg1.getParent());
    System.out.println(tg2.getName() + " parent: "
                       + tg2.getParent());
    
  }
}

Result
Group1 active threads: 0
Group1 active groups: 1
Group1 parent: java.lang.ThreadGroup[name=main,maxpri=10]
Group2 parent: java.lang.ThreadGroup[name=Group1,maxpri=10]
There are some interesting methods that I didn't show in the example above. To interrupt all threads in a thread group, use the interrupt() methd in ThreadGroup class. If we want to put active threads in a thread group into an array, use the enumerate() method. If we want to set and get thread group priority, use setMaxPriority() and getMaxPriority() methods. For more information, check out the ThreadGroup class.

yield() Method

yield() method gives a hint to the JVM scheduler that the thread that invoked yield() is willing to give up its CPU time which can be used by other threads. The thread scheduler may ignore the hint or not. Using this method effectively is not an easy task. You may need to use detailed profiling and benchmarking in order to achieve the desired effect that you wanna achieve. Let's create an example to introduce the yield() method.
public class SampleClass{

  public static void main(String[]args){
    Thread t1 = new Thread(new Counter("t1"));
    Thread t2 = new Thread(new Counter("t2"));
    
    t1.setPriority(Thread.MAX_PRIORITY);
    t2.setPriority(Thread.MIN_PRIORITY);
    
    t1.start();
    t2.start();
  }
}

class Counter implements Runnable{
  private String name;
  
  Counter(String name){
    this.name = name;
  }
  
  @Override
  public void run(){
  
    for(int i = 0; i < 2; i++){
      System.out.println(name +": " + i);
      Thread.yield();
    }
  }
}

Note: Result may vary
Result
t1: 0
t2: 0
t1: 1
t2: 1

No comments:

Post a Comment