Skip to content
Threads and Runnable
step 1/6

Reading — step 1 of 6

Learn

~3 min readConcurrency Lite

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 of t.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…