Multithreading in Java

Beknazar
6 min readDec 7, 2021

--

Multithreading or concurrency is one of the hardest topics to learn. In addition, the issues due to the multithreading are most annoying and difficult to locate.

All right, this is our agenda for today.

  • Understanding Thread Concurrency.
  • Multithreading with Thread
  • Multithreading with Runnable
  • Multithreading with ExecutorService

Understanding Thread Concurrency

I want to start with the definitions

thread — the smallest unit of execution that can be scheduled by the operating system.

process — group of associated threads that execute in the same environment.

system thread — thread created by JVM and runs in the background of the application.

For example, the java garbage collector thread is a system thread and runs in the background with our threads to clean up unused objects.

user-defined thread — created by the application developer to accomplish a specific task.

daemon thread — the thread that will not prevent the JVM from exiting when the program finishes. If a thread creates another thread, the created thread will be daemon if only the original thread is(basically it inherits this property).

threads priority — we can give different priorities to our threads. The threads with higher priority are executed in preference to threads with lower priority. Similarly, with the daemon flag, the priority of the thread gets inherited if a new thread is created from the given thread.

CPU is responsible for executing our program. However, our applications will not interact with the CPU directly instead they will interact with Operating System that will manage executions. Even though nowadays is not rare to have a multi-core or multi-CPU system, it is still not enough to run multiple applications that we run at once and have each thread to occupy a whole processor. The Operating Systems have complex thread-scheduling and context-switching algorithms that allow users to execute dozens or even hundreds of threads. These scheduling algorithms allow users to experience the illusion that multiple tasks were being performed at the exact same time within a single-CPU(or multi-CPU system).

Multithreading with Thread

One way of creating extra threads is by extending from Thread class.

How many in total threads(user-defined) got executed in the above program?

The answer is 3. The first one is our main method this is the default thread of our program. And then we start two more threads inside this thread.

So we need to extend Thread class and override run() method. Then we need to create an object of that class and just start in the thread with start() method. Whatever we put inside run() the method will be executed as a separate user-defined thread.

What’s the output from the above code? We don’t know. One possible output

Starting out threads..
The end.
one: 1
one: 2
two: 1
one: 3
two: 2
two: 3

Multithreading with Runnable

Another way of achieving multithreading is to implement Runnable interface.

One possible output:

Starting out threads..
The end.
one: 1
two: 1
one: 2
two: 2
one: 3
two: 3

We also have three threads here.

This time our class implements our Runnable interface and overrides the run method. To run we still need to pass an instance of our class to the Thread object and use the start method.

What’s the difference between these two options?

  • If you need to define your own rules for multithreading, such as a priority and so on, extending the Thread class is a good idea.
  • When using Runnable, you can let your class extend some other classes, but it will not work if extending the Thread class(java doesn’t support multiple inheritances).
  • Implementing Runnable allows the class to be used by a lot of Concurrency API classes.

Multithreading with ExecutorService

The ExecutorService is part of the framework that works with concurrency features, such as scheduling, and thread pooling. ExecutorService is an interface and has a few different implementations.

Methods of ExecutorService(not full list):

void execute(Runnable command) This method is inherited from the Executor interface. Basically, it can execute a given task in a new thread or in a pooled thread. The argument is a functional interface Runnable so it can accept the argument as a lambda expression.

Future<?> submit(Runnable task) This method works similarly to execute method but returns a Future that can be used to monitor the task.

void shutdown() It’s important to shut down the thread we created with our executor otherwise they will keep running(they don’t stop by themselves). This method will first stop accepting new threads to submit to the executor then it will wait for all already submitted tasks to finish. Once the task is completed, it shuts it down.

boolean isShutdown() return true if executor invoked shutdown method(stops accepting new threads). But it does not mean all threads are completed.

boolean isTerminated() it will return true when there is no task left executing by the executor(all tasks have completed).

List<Runnable> shutdownNow() attempts to stop all executing tasks. It returns a list of Runnable — awaiting tasks.

New Single Thread Executor

One possible output:

Start
From custom thread one: 0
From custom thread one: 1
From custom thread one: 2
End
From custom thread two: 0
From custom thread two: 1
From custom thread two: 2
  • newSingleThreadExecutor implementation can execute/submit only a single thread at a time.
  • The threads submitted by it are guaranteed to be in order. So basically when one submitted thread will finish execution, the next one will start.
  • The above example creates two custom threads, but they are not in parallel. The second submitted thread will wait for the first one and then it will start execution. You can see how End might appear in the middle of two executions, because it’s a statement from the main thread and of course it’s independent of submitted two.

New Fixed Thread Pool

One possible output:

Start
From custom thread one: 0
End
From custom thread two: 0
From custom thread one: 1
From custom thread two: 1
From custom thread one: 2
From custom thread two: 2
  • newFixedThreadPool creates a pool of threads based on its argument. In our example, we created a pool with 5 threads. The executor will use these 5 threads to submit threads in parallel. If the number of threads submitted exceeds pool size then the executor will wait for the next available one.
  • if we would create a pool of size 1, it would behave as newSingleThreadExecutor executor.

New Cached Thread Pool

service = Executors.newCachedThreadPool();
  • it creates a thread pool that creates new threads as needed. And it reuses the available old threads. So basically, there is no limit on the number of threads for this executor and it will reuse old threads once they are done with execution.

You can use ScheduledExecutorService(interface) with newSingleThreadScheduledExecutor and newScheduledThreadPool implementations to schedule or run in the specific intervals your threads.

That’s all for today, have a wonderful day!

References and resources used for this article:
1. OCP Book.
2. Official Java docs.

--

--