Java Concurrency

Java Concurrency

·

10 min read

Back in the early days, computers could execute only one program at a time. But now, modern computers are capable of running a whole lot of tasks at the same time which is achieved through multithreading. Java is one of the programming languages that have support for multithreading, which means it enables the developer to create multithreaded applications. In concurrent programming, there are two basic units of the execution which are processes and threads. In Java, concurrency is mostly concerned with threads. Now before moving ahead let's recall what is multithreading and how we see multithreading implemented in daily life.

What is Processes?

A process in the simplest term is an executing program. A process has a self-contained execution environment. A process generally has a complete, private set of basic run-time resources. In Particular, each process has its own memory space.

What is a Thread?

A thread is a lightweight process. In Java multithreaded execution is an essential feature. Every application made using Java has at least one thread or several, if the threads that do memory management or carry out program execution are taken into consideration but from the developer's point of view, an application has only one thread which is the main thread. The main thread can create additional threads as per requirement. Threads exist within a process. Threads share the process's resources which makes for efficient but problematic communication which can be worked out by introducing synchronization between threads of a process. Now the next thing we should know about is concurrency.

What is Concurrency?

Concurrency is the ability to do more than one thing at the same time. Concurrency doesn't need to involve multiple applications, running multiple parts of a single application is also termed concurrency.

What happens under the hood?

Ok! now we know that machines can run multiple tasks at a time, but how do they do it?. Some of you will argue that nowadays computers come with multiple processors, but we can achieve concurrency on single-processor machines as well, computers can execute way more tasks than the number of processors available. So how can multiple tasks be executed at the same time even on a single CPU? Well, it turns out that, they don't execute at the same instant of time. Concurrency doesn't imply parallel execution. When people say "Multiple tasks are executing at the same time", what they mean is that "multiple tasks are making progress at the same time". These tasks are executed in an interleaved manner. In this blog post, I won't be discussing how multithreading is achieved as it is a whole different topic, maybe I will cover that in future blogs. So in short, Concurrency doesn't mean Parallelism. In fact, Parallelism is impossible on a single processor system. Concurrency is a broad term and multithreading is achieving concurrency at the thread level. Now let's look forward to some examples of multithreading. Suppose a scenario, we are chatting with our friend on our smartphone when we hit a character on the keyboard the UI gets updated and we see what we have typed also the if the word is misspelled the keyboard corrects it. Many things are happening at the same time, the keyboard's word processor is formatting the text and is responding to the keyboard click event at the same time. We can also take the example of a music app that streams online songs. It reads the bytes from the network, decompresses it, decodes it to the appropriate format, and updates the UI all at the same time while we are listening to the album of our favorite artist.


Multithreading in java

Java has the support for multithreading from the very beginning we can make threads and can establish synchronization between them. These APIs are adequate for basic tasks, but higher-level APIs are needed for more advanced tasks, so with the advent of Java 5.0, some high-level concurrency features were introduced most of which are implemented in java.util.concurrent packages. We would be exploring some of them in this blog, now without further ado, let us begin.

Let's make a java program using the conventional method of implementing multithreading, there are many ways to implement multithreading, I'll be using lambda expression for the Runnable interface.

class Main {
    public static void main(String args[]) {
        Runnable runnable;
        runnable = () - > {
            for (int i = 1; i <= 10; i++) System.out.println(i + " ");
        };
        Thread thread = new Thread(runnable);
        thread.start();
        for (int j = 101; j <= 110; j++) System.out.println(j + " ");
    }
}

By the output of the above example, we can see that the output is mixed which means there is multithreading in the above example. Now what happening here is we are assigning a lambda expression to a runnable reference variable named r, we are making a new thread from the main thread and passing it the reference of the object to which the variable r is referring at this line necessary data structures are made and on t.start() the thread gets activated. Now there are 2 threads involved in the above example which are the main thread and the thread which we created.

Let's achieve the same using high-level APIs

import java.util.concurrent.*;
class main {
    public static void main(String args[]) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Runnable runnable = () - > {
            for (int i = 1; i <= 10; i++) System.out.println(i + " ");
        };
        executorService.submit(runnable);
        for (int j = 101; j <= 110; j++) System.out.println(j + " ");
        executorService.shutdown();
    }
}

In the above example, we can observe a mixed output, if we won't call shutdown for executor service object then the program is not going to terminate.

Now let's look at more examples of high-level API's for multithreading.

Suppose you want to create a thread that should do some work and return the result which is to be consumed by some other thread, before high-level API's this was done by extending class with Thread and then writing methods for fetching the result, or by using callbacks. But With the introduction of Future in Java 8, it became easy to return value from one thread to another let see how it can be done.

import java.util.concurrent.*;
class Main {
    public static void main(String args[]) {
        Callable < Integer > work = () - > {
            // this is the work that has to be done and results will be 532 (An assumption)
            TimeUnit.SECONDS.sleep(5);
            return 530;
        };
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future < Integer > future = executorService.submit(work);
        System.out.println(future.isDone());
        try {
            System.out.println(future.get());
        } catch (ExecutionException executionException) {
            System.out.println(executionException);
        } catch (InterruptedException interruptedException) {
            System.out.println(interruptedException);
        }
        executorService.shutdown();
        System.out.println(future.isDone());

    }
}

The output of the above code :

false
530
true

Here it should be noted that 530 is printed exactly after 5 seconds.

In the above example, reference variable work of type Callable has been assigned a lambda expression which means work is referring to an anonymous object which has implemented Callable, now this object contains work and that work is going to produce the result. By writing Future < Integer > future = executorService.submit(work); we have submitted the work to ExecutorService type object. Now if you are thinking that the submit method of executor service object is going to return the value once work is done but that might not be the case as we can see in the output, false is printed immediately when isDone() method of a future object is called which means the summit method of executor service object is returning something on the spot and is not waiting for work to be done, now whatever submit is returning we have captured its reference in a Future type reference variable named future. What's happening under the hood is that submit will return a reference of a future type object now the work that we submitted using submit is still in progress once the work is completed and the result will be returned, it will be somehow stored in the same Future object to which future variable is referring, now since work is still to be done when we called get method of Future object the main thread got blocked and its execution won't be resumed until the work is completed and the result is returned. When the work is done, the result will be stored in a future object then get method of Future object will return 530, after which the execution of the main thread would be resumed.

I think you have got a basic understanding of multithreading and how it can be achieved, so let's move forward to thread pools and synchronization between the threads. Consider the following example

import java.util.concurrent.*;
class Common {
    private String data;
    public void setData(String data, int threadNumber) {
        this.data = data;
        System.out.println(this.data);
        System.out.println("This thread having number " + threadNumber + " is going on to sleep for 5 seconds.");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException interruptedException) {
            System.out.println(interruptedException);
        }
        System.out.println(this.data);
    }
}
class Main {
    public static void main(String args[]) {
        Common common = new Common();
        String s1 = "Hello";
        String s2 = "Boys";
        String s3 = "Girls";
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        Runnable r1 = () - > {
            common.setData(s1, 1);
        };
        Runnable r2 = () - > {
            common.setData(s2, 2);
        };
        Runnable r3 = () - > {
            common.setData(s3, 3);
        };
        executorService.submit(r1);
        executorService.submit(r2);
        executorService.submit(r3);
        executorService.shutdown();
    }
}

The output of the above code

Hello
Girls
Hello
This thread having number 3 is going on to sleep for 5 seconds.
This thread having number 1 is going on to sleep for 5 seconds.
This thread having number 2 is going on to sleep for 5 seconds.
Girls
Girls
Girls

The expected output of the above example should have been something like the following.

Boys
This thread having number 2 is going on to sleep for 5 seconds.
Boys
Hello
This thread having number 1 is going on to sleep for 5 seconds.
Hello
Girls
This thread having number 3 is going on to sleep for 5 seconds.
Girls

The above output can vary as the order of threads execution is uncertain, but after waking up the setData method of Common object executing on all the threads are going to print the same data irrespective of what is passed to them. So we can say that setData is not thread-safe, which calls for synchronization between the threads. To achieve synchronization we can either declare the method that is being called from different threads synchronized using the synchronized keyword or we can call the setData method of Common from synchronized blocks or we make use of semaphore, but there is another way which was introduced in High-Level APIs that are Locks.

import java.util.concurrent.*;
import java.util.concurrent.locks.*;
class Common {
    private String data;
    private ReentrantLock lock = new ReentrantLock();
    public void setData(String data, int threadNumber) {
        lock.lock();
        this.data = data;
        System.out.println(this.data);
        System.out.println("This thread having number " + threadNumber + " is going on to sleep for 5 seconds.");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException interruptedException) {
            System.out.println(interruptedException);
        }
        System.out.println(this.data);
        lock.unlock();
    }
}
class Main {
    public static void main(String args[]) {
        Common common = new Common();
        String s1 = "Hello";
        String s2 = "Boys";
        String s3 = "Girls";
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        Runnable r1 = () - > {
            common.setData(s1, 1);
        };
        Runnable r2 = () - > {
            common.setData(s2, 2);
        };
        Runnable r3 = () - > {
            common.setData(s3, 3);
        };
        executorService.submit(r1);
        executorService.submit(r2);
        executorService.submit(r3);
        executorService.shutdown();
    }
}

The output of the above code

Hello
This thread having number 1 is going on to sleep for 5 seconds.
Hello
Boys
This thread having number 2 is going on to sleep for 5 seconds.
Boys
Girls
This thread having number 3 is going on to sleep for 5 seconds.
Girls

In the above example, we learned how to achieve synchronisation between threads. Making use of Locks enables us to specify which part of the method needs to be synchronised and we don't need to synchronize the whole method.

Conclusion

In this article, we learned about the basics of concurrency and how it can be achieved in java using high-level APIs. Though there are much more things that were introduced in high-level APIs maybe I'll cover that in future blogs. In anyways, if you found this blog knowledgeable then please share it and do check out my other articles on the hashnode (Utsav Jain). In case you have any doubt or queries regarding this article feel free to comment below.