Threads and Concurrency
Doing two things at once
Everything you have written so far runs one statement after another. A thread is an independent line of execution — with several, parts of your program run at the same time.
The typical reason is that one task should not block another. Reading a slow sensor, processing a camera frame, or waiting on a network response can all proceed while the rest of the program keeps going.
Thread worker = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("worker: " + i);
}
});
worker.start(); // begins running alongside main
for (int i = 0; i < 3; i++) {
System.out.println("main: " + i);
}
The output interleaves, and the exact order changes between runs. That unpredictability is the defining feature of concurrent code, and the source of most of its difficulty.
start(), not run()
worker.start(); // runs on a NEW thread
worker.run(); // runs on the CURRENT thread — just a normal method callCalling run() compiles and executes the code, but sequentially, with no second thread. Nothing appears wrong; you simply do not get concurrency. This is the single most common beginner mistake here.
Creating threads
Three forms, all equivalent in effect:
// 1. lambda — clearest for short tasks
Thread t = new Thread(() -> System.out.println("hello"));
// 2. a Runnable stored first
Runnable task = () -> System.out.println("hello");
Thread t = new Thread(task);
// 3. extending Thread — rarely the right choice
class Worker extends Thread {
@Override public void run() { System.out.println("hello"); }
}
Prefer the first two. Extending Thread spends your one inheritance slot and couples the task to the mechanism running it — the task is a Runnable, and how it is executed should be a separate decision.
Waiting for a thread
start() returns immediately. To wait for a thread to finish, join() it:
Thread worker = new Thread(() -> {
heavyCalculation();
});
worker.start();
doSomethingElse(); // happens concurrently
worker.join(); // blocks until worker finishes
System.out.println("both done");
join() throws InterruptedException, so it must be handled:
try {
worker.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore the flag
}
Restoring the interrupt flag matters. Catching InterruptedException clears it, and swallowing it silently means code further up can no longer tell it was asked to stop.
Sleeping
try {
Thread.sleep(1000); // milliseconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
sleep pauses the current thread. It is useful for pacing a loop, but it is not a synchronisation tool — sleeping “long enough” for another thread to finish is a guess that will eventually be wrong.
Why this is hard
Concurrency introduces a class of bug that sequential code cannot have.
What makes concurrent code difficult
- Non-determinism. The interleaving differs each run, so a bug may appear once in a thousand executions and never under a debugger.
- Shared state. Two threads touching the same variable can corrupt it, even for operations that look atomic. See Race Conditions.
- Visibility. A change made by one thread is not guaranteed to be seen by another without proper synchronisation.
- Deadlock. Two threads can each wait for something the other holds, and both stop forever.
- Testing is unreliable. Passing a test proves the bug did not happen this time.
Because of this, the guiding principle is to share as little mutable state as possible. A thread working only on its own data is straightforward; the trouble starts when two threads write the same thing.
Thread states
| State | Meaning |
|---|---|
NEW | Created but start() not yet called |
RUNNABLE | Running, or ready and waiting for a processor |
BLOCKED | Waiting to acquire a lock |
WAITING | Waiting indefinitely for another thread |
TIMED_WAITING | Waiting with a timeout, such as during sleep |
TERMINATED | Finished |
Thread.getState() reports these, which is occasionally useful when diagnosing a hang.
Daemon threads
A daemon thread does not keep the program alive:
Thread background = new Thread(this::pollSensor);
background.setDaemon(true); // must be set BEFORE start()
background.start();
Java exits when all non-daemon threads finish. Use daemon threads for background work that should not prevent shutdown — and remember they are killed abruptly, so never use one for work that must complete.
A note on robot code
FTC and FRC frameworks call your code on a fixed loop, usually every 20 milliseconds. That loop is already the concurrency model, and adding threads is usually the wrong instinct.
Before adding a thread to robot code
- Blocking the main loop is the real problem. If a task takes too long, the fix is usually to break it into steps across loop iterations rather than to spawn a thread.
- Hardware access is often not thread-safe. Reading a motor or sensor from two threads can produce garbage or throw.
- The framework may already provide a mechanism — a notifier, a separate vision thread, an async command. Use that instead of a raw
Thread.
Genuine cases exist, mainly long-running independent work like image processing. Even then, share as little as possible with the main loop and share it safely.
Common mistakes
- Calling
run()instead ofstart(). - Assuming an order. Without synchronisation there is none.
- Swallowing
InterruptedExceptionwithout restoring the flag. - Using
sleepto coordinate threads. Usejoinor a proper synchroniser. setDaemonafterstart(), which throwsIllegalThreadStateException.- Starting the same
Threadtwice. Also throws — create a new one. - Creating threads in a loop. Each carries real cost; use a pool. See Executors and Thread Pools.
Practice
- Start a thread printing 1 to 5 while the main thread prints A to E. Run it several times and note the order changes.
- Modify it so main waits for the worker with
join(), and confirm the output is now ordered. - Write a thread that sleeps one second then prints, and confirm main continues meanwhile.
- Call
run()instead ofstart()and observe that the interleaving disappears. - Start a daemon thread looping forever, and confirm the program still exits when main finishes.
Hints
- Run it at least five times — a single run proves nothing about ordering.
join()after the main loop.- Print a timestamp before and after to see the overlap.
- This is the mistake worth making deliberately once.
setDaemon(true)beforestart(). Try it without and see that the program hangs.