Threads Made Simple: From Zero to Hero
Part 1: What is a thread?
A thread is one path of work inside a program. A program can have one thread or many, and each thread can do its own job at the same time as the others.
Think of a restaurant:
- The restaurant is your program.
- The kitchen is the shared space, which is the program’s memory.
- The cooks are your threads.
If one cook makes every dish in order, customers wait a long time. If three cooks work at once, the food comes out faster. The cooks share the same kitchen, though, so they need rules. Two cooks should not grab the same knife at the same moment. Programmers have the same problem, and we solve it with tools called locks (more on that later).
Part 2: What does a thread do?
Threads help in three main ways:
- Speed: Heavy work can be split in
- to pieces, and each piece runs on a different thread. On a multi-core CPU, these pieces can run at the same time.
- Responsiveness: A slow task, like downloading a file, can run on a background thread. The screen keeps responding to clicks while the download continues.
- Doing many things together: A web server can handle many visitors at once, with one thread per visitor, or a group of threads shared among them.
Part 3: How does a thread work?
Here is what happens step by step:
- Every program starts with one thread, called the main thread. In C++ it runs
main(). In Java it also runsmain(). - The main thread creates a new thread and gives it a job (a function to run).
- The operating system (Windows, Linux, macOS) decides which thread runs on which CPU core. If there are more threads than cores, it switches between them very quickly, so they look like they run together.
- The main thread can wait for the new thread to finish (
join), or keep doing its own work.
One important fact: the operating system decides the timing. So the output order can be different every time you run the program.
Part 4: Threads in C++
C++ added built-in thread support in C++ 11 through the <thread> library.
Your first thread
#include <iostream>
#include <thread>
void sayHello(int id) {
std::cout << "Hello from thread " << id << "\n";
}
int main() {
std::thread t1(sayHello, 1); // start thread 1
std::thread t2(sayHello, 2); // start thread 2
t1.join(); // wait for thread 1 to finish
t2.join(); // wait for thread 2 to finish
std::cout << "Both threads are done.\n";
}Compile on Linux or macOS with g++ file.cpp -o prog -pthread.
Key rule: You must call join() or detach() on every thread. If you forget both, the program will crash when the thread object is destroyed.
Checking how many threads your machine supports
#include <iostream>
#include <thread>
int main() {
std::cout << "Hardware threads: "
<< std::thread::hardware_concurrency() << "\n";
}The danger: race conditions
When two threads change the same variable at the same time, the result can be wrong:
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
std::mutex m; // a lock: only one thread can hold it at a time
void work() {
for (int i = 0; i < 1000; i++) {
std::lock_guard<std::mutex> lock(m); // lock while changing counter
counter++; // lock releases automatically
}
}
int main() {
std::thread t1(work);
std::thread t2(work);
t1.join();
t2.join();
std::cout << "Counter: " << counter << "\n"; // always 2000
}Without the mutex, the final number could come out lower than 2000, because both threads might read the same old value and overwrite each other’s update.
Get Najmudheen A’s stories in your inbox
Join Medium for free to get updates from this writer.
Newer tip (C++20): std::jthread works like std::thread, but it joins automatically when it goes out of scope, so you cannot forget.
Part 5: Threads in Java
Java has had threads since its very first version, and they are part of the language itself.
Your first thread
Java gives you two main ways to create a thread.
Way 1: Extend the Thread class
class HelloThread extends Thread {
private final int id;
HelloThread(int id) { this.id = id; }
@Override
public void run() { // the job the thread does
System.out.println("Hello from thread " + id);
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t1 = new HelloThread(1);
HelloThread t2 = new HelloThread(2);
t1.start(); // starts a new thread
t2.start();
t1.join(); // wait for thread 1
t2.join(); // wait for thread 2
System.out.println("Both threads are done.");
}
}Way 2: Use a Runnable (the more common and flexible approach)
public class Main {
public static void main(String[] args) throws InterruptedException {
Runnable job = () -> System.out.println("Hello from a lambda thread");
Thread t = new Thread(job);
t.start();
t.join();
}
}Important: Call start() to run the thread in the background. If you call run() directly, Java runs the code on the current thread, and no new thread is created.
The danger: race conditions in Java
public class Main {
static int counter = 0;
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable work = () -> {
for (int i = 0; i < 1000; i++) {
synchronized (lock) { // only one thread at a time
counter++;
}
}
};
Thread t1 = new Thread(work);
Thread t2 = new Thread(work);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Counter: " + counter); // always 2000
}
}The synchronized keyword works like a C++ mutex. It makes sure only one thread enters that block at a time.
A modern tool: ExecutorService
Creating a new thread for every small job is wasteful. Java offers thread pools, which reuse a fixed group of threads:
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(3); // 3 reusable workers
for (int i = 1; i <= 5; i++) {
int taskId = i;
pool.submit(() ->
System.out.println("Task " + taskId + " on " + Thread.currentThread().getName()));
}
pool.shutdown(); // no new tasks accepted
pool.awaitTermination(5, TimeUnit.SECONDS); // wait for tasks to finish
System.out.println("All tasks finished.");
}
}Five tasks share three workers, so the workers are reused instead of created again and again. Java 21 also added virtual threads, which are very lightweight, so a program can run thousands of them without trouble.
Part 6: C++ and Java side by side
The core ideas are the same in both languages: start a worker, wait for it, and protect shared data with a lock.
Part 7: Common mistakes beginners make
- Forgetting
join()in C++. Always join or detach each thread. - Calling
run()instead ofstart()in Java. No new thread is created. - Sharing data without a lock. This causes race conditions that appear randomly and are hard to find.
- Using too many threads. More threads are not always faster. Extra threads cost memory, and too many switches slow things down.
- Expecting a fixed output order. Thread timing changes between runs, so never assume one thread finishes first.
- Holding a lock too long. Other threads must wait, which removes the benefit of threading.
Part 8: Key words to remember
Conclusion
A thread is simply a worker inside your program. You give it a job, let it run, and wait for it when you need the result. The main challenges are sharing data safely and making sure every thread is properly managed. Once you understand the restaurant picture, both C++ and Java threads will feel familiar.
Your challenge: Write a program in C++ or Java that starts two threads. Each thread should print the numbers 1 to 5 with its name, such as “Thread A: 3”. Run the program five times and notice how the order changes each time. Then add a lock so that each thread prints its five lines without being interrupted.
Happy threading!









