Java concurrency in practice: threads

Kostiantyn Ivanov
12 min readSep 28, 2023

--

What thread is?

Java threads are a higher-level abstraction provided by the Java programming language and the Java Virtual Machine (JVM). They are managed by the JVM and the Java runtime environment.

Java threads are used to achieve concurrency within a Java application. You can create and manage Java threads in your Java code using classes from the java.lang.Thread package.

Java threads are lightweight compared to operating system threads (CPU threads). They are managed by the JVM and are scheduled to run on the available CPU threads provided by the operating system.

Threads in Java allow your program to multitask, performing different operations concurrently. It’s like having multiple “workers” doing different jobs within the same program, making it more responsive and efficient, especially when handling tasks like responding to user input while performing background operations.

History

Java 1.0 (1996):

Java introduced the concept of threads in its first version. The java.lang.Thread class was introduced to represent a thread of execution. The Runnable interface provided a way to define the code to be executed by a thread.

Java 1.1 (1997):

The Thread class was enhanced to include methods like yield(), sleep(), and stop() for thread management.

Functionality

Thread class methods:

start():
This method starts the execution of a thread. When you call start(), it internally calls the run() method of the thread, and the thread begins executing.

run():
This method is the entry point for the code that runs in the thread. You override this method to specify the task that the thread should perform. You should not call run() directly; instead, you call start() to begin the execution of the thread.

sleep(long milliseconds):
This method causes the current thread to pause execution for the specified number of milliseconds. It can be used for introducing delays in a thread’s execution.

join():
The join() method allows one thread to wait for another thread to complete its execution (means dead, you will not continue you execution if the the thread you are waiting for in the “WAITING” state as well). When you call join() on a thread, the calling thread will wait until the target thread finishes. You can also use overloaded versions of join() to specify a timeout.

wait()(from object class but related):
The primary purpose of wait() is to make a thread voluntarily give up the CPU and enter a waiting state. It is often used when a thread needs to wait for a certain condition to be met or for another thread to notify it.

notifyAll()(from object class but related):
notifyAll() is used to signal to all waiting threads that a specific condition or state they were waiting for has changed, and they should reevaluate and potentially continue their execution.
It’s typically used in situations where multiple threads are waiting for a shared resource or condition to change.

interrupt():
This method interrupts a thread’s execution. It sets the interrupted status of the thread to true. You can use this in combination with methods like isInterrupted() to gracefully stop a thread.

isAlive():
This method checks whether a thread is still alive (i.e., has not yet completed its execution).

getId():
This method returns a unique identifier for the thread.

setName(String name) and getName():
These methods allow you to set and retrieve the name of the thread, which can be useful for identifying threads in logs and debugging.

setPriority(int priority) and getPriority():
These methods allow you to set and retrieve the priority of a thread. Thread priorities can influence how the operating system schedules threads for execution. But it’s not guaranteed that thread with higher priority will be more frequent.

yield():
The yield() method is a hint to the scheduler that the current thread is willing to yield its current execution time to allow other threads to run. It doesn't guarantee that the scheduler will switch to another thread.

isDaemon() and setDaemon(boolean on):
These methods allow you to check if a thread is a daemon thread (a thread that runs in the background and does not prevent the JVM from exiting) and set a thread as a daemon.

currentThread():
This is a static method that returns the currently executing thread.

getState():
This method returns the current state of the thread, represented by an enum value from the Thread.State enum. Possible states include NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED.

Thread states:

Thread states can change as a thread executes and interacts with other threads. Here are the different thread states and how they transition from one to another:

NEW:
A thread is in the NEW state when it has been created but has not yet started executing.
To move from the NEW state to the RUNNABLE state, you typically call the start() method on the thread object. This method initiates the thread's execution, and it transitions to the RUNNABLE state.

RUNNABLE:
A thread is in the RUNNABLE state when it is eligible to run but may not necessarily be executing at a given moment.
The RUNNABLE thread can transition to the RUNNING state when the operating system's thread scheduler assigns CPU time to it for execution.

BLOCKED:
A thread is in the BLOCKED state when it is waiting to acquire a lock (monitor) that is currently held by another thread.
It transitions to the RUNNABLE state when it successfully acquires the lock and is ready to execute.

WAITING:
A thread enters the WAITING state when it is waiting for a specific condition to be met, often due to methods like wait() or join().
It remains in this state until another thread interrupts it, notifies it, or the specified condition is met.

TIMED_WAITING:
Similar to the WAITING state, a thread is in the TIMED_WAITING state when it is waiting for a condition, but it will automatically transition back to RUNNABLE after a specified time period if the condition is not met.
Common methods that can cause this state include sleep() and wait(timeout).

TERMINATED:
A thread enters the TERMINATED state when it has completed its execution or when an unhandled exception causes it to terminate.
Once a thread is in the TERMINATED state, it cannot transition to any other state.

Example of usage

Let’s suppose we decided to implement our own HTTP server (it’s not something we have to create each time, but it explains well how our calls handled by existing solutions and bay be a good place to explain opportunities that threads bring us).
Here is a super-simple implementation, that listens 8080 port and return “Hello, World!” with 200 status code to each request.

public class OneThreadHttpServer {
public static void main(String[] args) {
int port = 8080;

try {
// Create a server socket on port 8080
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("Server listening on port " + port);

while (true) {
// Accept incoming client connections
Socket clientSocket = serverSocket.accept();
System.out.println("Accepted connection from " + clientSocket.getInetAddress());

// Handle the client request
handleClientRequest(clientSocket);

clientSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

private static void handleClientRequest(Socket clientSocket) {
try(OutputStream output = clientSocket.getOutputStream()) {
StringBuilder response = new StringBuilder();
String responseBody = "Hello, World!";
addHeader(response, responseBody.length());
response.append(responseBody);
output.write(response.toString().getBytes());
output.flush();

} catch (IOException e) {
e.printStackTrace();
}
}

private static void addHeader(StringBuilder sb, int contentLength) {
sb.append("HTTP/1.1 200 OK\n")
.append("Content-type: plain/text\n")
.append("Content-length: ").append(contentLength)
.append("\n")
.append("\n");
}
}

Lets test it:

HTTP verbs, request data are not in the scope of this article. The only on interesting property for us is a latency time. Now we have 2ms response.

Looks good. But what if the “business logic” will take some time? Let’s add the delay into the code and see what will happen:

private static void handleClientRequest(Socket clientSocket) {
try(OutputStream output = clientSocket.getOutputStream()) {
...
response.append(responseBody);
Thread.sleep(10000);
...

} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

As expected the latency was increased up to 10 seconds:

What if we will call our server from another window in the same time? Will we receive the response in 10 seconds as well?

Looks like, no… The seconds call waited until the first call will be executed and only then took the main thread.

How Thread class can help us to improve our HTTP server and make it more efficient?(In the real world we have a more modern abstractions than thread and we will discover them in the next articles of this series but for not we are able to use threads only and it’s actually a base for a lot of future information) Let’s see.

Worker class

public class Worker extends Thread {

private final Socket clientSocket;

public Worker(Socket clientSocket) {
this.clientSocket = clientSocket;
}

@Override
public void run() {
super.run();

try(OutputStream output = clientSocket.getOutputStream()) {
StringBuilder response = new StringBuilder();
String responseBody = "Hello from thread [" + Thread.currentThread().getId() + "]";
addHeader(response, responseBody.length());
response.append(responseBody);
Thread.sleep(10000);
output.write(response.toString().getBytes());
output.flush();

clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

private void addHeader(StringBuilder sb, int contentLength) {
sb.append("HTTP/1.1 200 OK\n")
.append("Content-type: plain/text\n")
.append("Content-length: ").append(contentLength)
.append("\n")
.append("\n");
}
}

We created a class extending the Thread class and move all the request/response handling logic there.

MultithreadHttpServer class

public class MultithreadThreadHttpServer {
public static void main(String[] args) {
int port = 8080;

try {
// Create a server socket on port 8080
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("Server listening on port " + port);

while (true) {
// Accept incoming client connections
Socket clientSocket = serverSocket.accept();
System.out.println("Accepted connection from " + clientSocket.getInetAddress());
// Handle the client request
new Worker(clientSocket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

Here we have similar logic as in the previous version but for each accepted call from the socket we create a new Worker instance and call its “start” method.

How the new implementation works?
We make calls from three separate windows:

As we can see — we have the similar latency for rach of the calls and each call was processed by different java thread.

This is how it looks like:

So, all the “logic” (sleeping threads) are covered by threads 0-N meanwhile the main thread is ready to receive connections.

But let’s put some more realistic load to our server. Let’s emulate 300 parallel calls for example

We can handle a lot of java threads, but what the heap consumption do we have?

Well. Looks like unlimited threads are not free and considering the fact we don’t have any real logic — 150MB it’s pretty significant value. And what will be if 1000 calls will come? 1M? Probably we still need a main thread to stop receive connections but only when the we reach some limit of threads. Another aspect that would be great to improve — Thread instantiation is pretty heavy operation, so potentially we don’t want to create them each time but only once (until the same thread limit will be reacted).

Worker:

public class Worker extends Thread {
private Socket clientSocket;
private volatile boolean hasJob = false;

public Worker(Socket clientSocket) {
this.clientSocket = clientSocket;
}

public boolean hasJob() {
return hasJob;
}

public void updateForNewJob(Socket clientSocket) {
synchronized (this) {
this.clientSocket = clientSocket;
this.hasJob = true;
notifyAll();
}
}

@Override
public void run() {
while (true) {
processJob();
while (!hasJob) {
synchronized (this) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}

private void processJob() {
hasJob = true;
try(OutputStream output = clientSocket.getOutputStream()) {
...
Thread.sleep(10000);
synchronized (this) {
notifyAll();
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
hasJob = false;
}
...
}

So, we have new more features in our Worker:
- We added the flag “hasJob” indicating that this worker is busy we set this flag=true when we start our “business logic” and set flag=false when we finish our work.
- After the job was done — our thread go to the “WAITING” state, until its “hasJob” state will not come “true”.
- After the job was done(but before waiting) we notify all the waiters on our thread, that we are ready to take new job.
- updateForNewJob — sets the new client socket to work and says, that we have a new job (changes the flag)

We used a volatile modificator to our flag.

NOTE: Visibility Guarantee:

  • The volatile keyword ensures that when one thread writes to a volatile variable, the change is immediately visible to other threads reading the same variable.
  • Without volatile, a thread may cache the variable's value in its own local memory, leading to inconsistent and outdated values when read by other threads.

MultithreadThreadHttpServer:


public class MultithreadThreadHttpServer {

private static final int THREADS_LIMIT = 100;
private static final Worker[] THREAD_POOL = new Worker[THREADS_LIMIT];
private static final Random RANDOM = new Random();

private int capacity;

public static void main(String[] args) {
new MultithreadThreadHttpServer().start();
}

private void start() {
int port = 8080;

try {
// Create a server socket on port 8080
...

while (true) {
// Accept incoming client connections
...
if (capacity < THREADS_LIMIT) {
// Handle the client request
Worker worker = new Worker(clientSocket);
THREAD_POOL[capacity++] = worker;
worker.start();
} else {
Worker worker = findFirstFreeOrRandom();

while (worker.hasJob()) {
synchronized (worker) {
worker.wait();
}
}
worker.updateForNewJob(clientSocket);
}
}
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

private Worker findFirstFreeOrRandom() {
for (Worker worker : THREAD_POOL) {
if (!worker.hasJob()) {
return worker;
}
}

return THREAD_POOL[RANDOM.nextInt(THREADS_LIMIT)];
}
}

Here we added:

  • Thread limit — limits a number of thread we will create
  • THREAD_POOL — arrays of created threads (We are not going to remove them. The tread was created once will be alive waiting for a new job)
  • Until we reach the thread limit — we work as previously — just create a new thread and run the job
  • If the limit was reached — we take the first thread without job or any random thread from the thread pool (if all the threads have jobs)
  • If a thread from the pool is ready to take a new job — we update it with new client socket and status
  • If thread is still working — we starting waiting on its monitor until it will be ready (meanwhile the main thread will be in a “WAITING” state and will not take new connections.

Let’s see how does it work:

We call our server from 100 parallel sessions (current thread limit):

We have all the 100 created, proceed with job and start waiting.
The heap consumption is next:

During the waiting state we can check, that our threads are not cinsume too much resources:

Let’s call our server using 1000 parallel connections:

The number if threads is still the same and they work with a small pauses of waiting consuming next connections:

The memory consumption is pretty much the same:

If we want to limit a number of waiting connections — we can create a separate queue for them in our server memory and reject all the connections after this queue will be fulfilled.

Summary

We discovered java Threads and how they can assist us to parallelize our applications. It’s a base concept that will be somewhere under the hood of more modern and powerful tools. We will review them in the next articles of this series.

Links

Sources with examples: https://github.com/sIvanovKonstantyn/java-concurrency

Other articles of series:

--

--