Reading — step 1 of 6
Learn
A thread is a separate path of execution within your program. Use threads to do work in parallel — handling multiple network requests, running background tasks, or speeding up CPU-intensive computation across cores.
Creating a thread
The modern way uses Runnable (a functional interface — single method run()):
Runnable task = () -> {
System.out.println("running on " + Thread.currentThread().getName());
};
Thread t = new Thread(task);
t.start(); // starts the thread; run() is called by the runtime
t.join(); // wait for it to finish
Don't call t.run() directly — that runs synchronously on the current thread. Always use t.start().
Why threads are useful
Without threads, all your code runs sequentially. Long network calls, file reads, computations — everything blocks. With threads:
Runnable downloadA = () -> download("a.zip");
Runnable downloadB = () -> download("b.zip");
Thread t1 = new Thread(downloadA);
Thread t2 = new Thread(downloadB);
t1.start();
t2.start();
t1.join(); // wait for both
t2.join();
// total wait ≈ max(time_a, time_b), not sum
Daemon threads
Daemon threads don't prevent the JVM from exiting. Use them for background work:
Thread bg = new Thread(() -> {
while (true) {
cleanupCache();
Thread.sleep(60_000);
}
});
bg.setDaemon(true);
bg.start();
Without setDaemon(true), your program won't exit while this thread is running.
Sleep and interrupt
Thread.sleep(1000); // 1 second — throws InterruptedException
Thread t = new Thread(...);
t.start();
t.interrupt(); // signal: please stop. The thread should check
// Thread.currentThread().isInterrupted() periodically.
Interruption is cooperative — the thread has to check the flag. There's no kill for threads.
Shared state — the danger zone
// BUGGY — race condition
int counter = 0;
Runnable inc = () -> {
for (int i = 0; i < 1000; i++) counter++; // not atomic!
};
Thread t1 = new Thread(inc);
Thread t2 = new Thread(inc);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter); // probably NOT 2000
Why buggy: counter++ is read-modify-write. Two threads can read the same value, both increment, both write — losing one update.
Fixes:
// 1. AtomicInteger — lock-free atomic operations
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // atomic
// 2. synchronized — mutual exclusion
int counter = 0;
final Object lock = new Object();
synchronized (lock) {
counter++; // critical section
}
// 3. ReentrantLock — explicit lock with more features
Lock lock = new ReentrantLock();
lock.lock();
try { counter++; } finally { lock.unlock(); }
For most cases, AtomicInteger (or other Atomic* classes) is the simplest and fastest option.
Why threads matter (and where they hurt)
Good: parallel I/O (network, disk), parallelizable computation across cores, responsive UIs, background cleanup.
Dangerous: shared mutable state without synchronization → race conditions, deadlocks, non-deterministic bugs that only show up under load.
In modern Java, prefer ExecutorService (covered in Java Advanced) over raw Thread — managed pools, cleaner lifecycle, better for production.
Common mistakes
- Calling
t.run()instead oft.start()— runs synchronously, no parallelism. - Sharing mutable state without synchronization — races, lost updates, sometimes silent data corruption.
- Using non-daemon threads for background tasks — JVM won't exit cleanly.
- Holding a lock during a long operation — blocks every other thread waiting for it.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…