Concurrency in Java

Table of contents
- Creating Threads in Java
- Starting Threads with .start()
- Pausing Execution with Thread.sleep(long millis)
- Observing Thread Execution with the Supervisor Pattern
- Blocking on Thread Completion with .join()
- Synchronizing Access to Shared Resources
- Communicating Between Threads with .wait() and .notify()
- Conclusion

Concurrency is a fundamental concept in Java that enables multiple tasks to run in parallel. Java provides a robust multithreading model that helps in improving application performance, responsiveness, and efficient resource utilization. This article explores different ways to create and manage threads, synchronization techniques, and inter-thread communication.
Creating Threads in Java
There are three primary ways to create threads in Java:
Extending the Thread
Class
The simplest way to create a thread is by extending the Thread
class and overriding the run
method.
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread running: " + i);
}
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
Implementing the Runnable
Interface
Another approach is to implement the Runnable
interface and pass it to a Thread
object.
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Runnable thread running: " + i);
}
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
Using Lambda Expressions
With Java 8 and later, we can use lambda expressions to define a Runnable
more concisely.
public class LambdaThreadExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Lambda thread running: " + i);
}
});
thread.start();
}
}
Starting Threads with .start()
The start()
method begins execution of a new thread.
public class StartThreadExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Thread started"));
thread.start();
}
}
Pausing Execution with Thread.sleep(long millis)
We can pause a thread using Thread.sleep()
.
public class SleepExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
System.out.println("Thread sleeping: " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
Observing Thread Execution with the Supervisor Pattern
A supervisor thread can monitor other threads and take action when needed.
public class SupervisorPattern {
public static void main(String[] args) {
Thread worker = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Worker thread running: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Worker interrupted");
}
}
});
worker.start();
Thread supervisor = new Thread(() -> {
while (worker.isAlive()) {
System.out.println("Supervisor: Worker is still running...");
try {
Thread.sleep(700);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Supervisor: Worker has finished.");
});
supervisor.start();
}
}
Blocking on Thread Completion with .join()
The join()
method makes one thread wait for another to complete.
public class JoinExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("Worker thread finished");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
try {
thread.join();
System.out.println("Main thread continues after worker thread");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Synchronizing Access to Shared Resources
Using synchronized
prevents race conditions when multiple threads access shared data.
class SharedResource {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class SynchronizedExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
resource.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
resource.increment();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + resource.getCount());
}
}
Communicating Between Threads with .wait()
and .notify()
Threads can communicate using wait()
and notify()
.
class SharedQueue {
private boolean dataAvailable = false;
public synchronized void produce() {
dataAvailable = true;
notify();
}
public synchronized void consume() {
while (!dataAvailable) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Data consumed");
}
}
public class WaitNotifyExample {
public static void main(String[] args) {
SharedQueue queue = new SharedQueue();
Thread producer = new Thread(queue::produce);
Thread consumer = new Thread(queue::consume);
consumer.start();
producer.start();
}
}
Conclusion
Concurrency in Java is powerful but requires careful management to avoid race conditions, deadlocks, and inefficiencies. By understanding thread creation, synchronization, and communication, developers can build efficient and responsive applications.
Subscribe to my newsletter
Read articles from Ali Rıza Şahin directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ali Rıza Şahin
Ali Rıza Şahin
Product-oriented Software Engineer with a solid understanding of web programming fundamentals and software development methodologies such as agile and scrum.