Discussion points for concurrency
- What does it mean for a program to perform more than one task at a time?
- What kinds of applications need to remain responsive while work happens in the background?
- What is a program doing while waiting for a file, database or network response?
- Can another task make progress during that waiting time?
- Does adding threads always make a program faster?
- What is a process?
- What is a thread?
- What is the relationship between a Java application and its threads?
- Does a Java application already have a thread before we create one?
- Which resources are shared by threads in the same process?
Consider:
firstTask();
secondTask();
thirdTask();
- When can
secondTask()begin? - What makes this sequential execution?
- How would the behavior change if firstTask and secondTask ran on a different thread?
- Must concurrent tasks finish in the order in which they started?
- Are concurrency and parallelism the same?
- Can a single-core computer run concurrent software?
- What does the operating-system scheduler do?
- When are tasks actually running in parallel?
- Why can concurrency still be useful without parallel execution?
Key idea: Concurrency concerns handling overlapping tasks. Parallelism means tasks physically execute at the same instant.
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}
- Which thread executes
main()? - What does
Thread.currentThread()return? - Why is its name usually
main? - Can the main thread create additional threads?
- What happens to the main thread after another thread is started?
Thread worker = new Thread(() -> {
System.out.println("Hello from a worker");
});
worker.start();
- When is the
Threadobject created? - When does the new path of execution begin?
- Which code does the worker execute?
- Can we predict exactly when it starts?
- Who decides when the worker receives CPU time?
Key idea: Creating a Thread object does not start it. Calling start() asks the JVM to schedule it.
Compare:
worker.start();
and:
worker.run();
- Which starts a new thread?
- Which is an ordinary method call?
- Which thread executes
run()in each case? - How could printing the thread name demonstrate the difference?
- Why is calling
run()directly a common mistake?
Key idea: start() creates a new path of execution that invokes run(). Calling run() directly uses the current thread.
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Performing a task");
}
};
- Is a
Runnablea thread? - What does it represent?
- How many abstract methods does the interface contain?
- What is the return type of
run()? - Can
run()declare checked exceptions? - Can a
Runnableexecute without a new thread?
Key idea: A Runnable describes work. It does not decide where or how that work is executed.
The interface is conceptually:
@FunctionalInterface
public interface Runnable {
void run();
}
- What is a functional interface?
- Why must it have one abstract method?
- How does Java know the lambda implements
run()? - What parameters does the lambda receive?
- What value does it return?
Supplier
Runnable first = new Runnable() {
@Override
public void run() {
System.out.println("Working");
}
};
Runnable second = () -> {
System.out.println("Working");
};
- Do these represent the same general behavior?
- What boilerplate does the lambda remove?
- Which version is easier to read?
- When might an explicit class still be clearer?
- Does a lambda automatically run concurrently?
Key idea: A lambda is compact syntax for implementing a functional interface. It does not create or start a thread.
String message = "Hello";
Runnable task = () -> {
System.out.println(message);
};
- What does it mean that the lambda captures
message? - Can the task read the variable?
- Can
messagebe reassigned afterward? - What does “effectively final” mean?
- Could a captured object still be mutable?
Key idea: A lambda can capture local variables that are final or effectively final.
for (int i = 0; i < 5; i++) {
int taskNumber = i;
Thread worker = new Thread(() -> {
System.out.println(taskNumber);
});
worker.start();
}
- Why is
taskNumbercreated inside the loop? - Why can the lambda capture it?
- Will the values necessarily be printed in numerical order?
- What determines the output order?
Key idea: Each lambda captures a stable value, but thread scheduling still makes execution order unpredictable.
worker.start();
worker.join();
System.out.println("Worker finished");
- Which thread waits when
join()is called?
Key idea: join() makes the calling thread wait until another thread terminates.
Compare:
first.start();
first.join();
second.start();
second.join();
with:
first.start();
second.start();
first.join();
second.join();
- Which version allows the tasks to overlap?
- Why is the first version effectively sequential?
- Does joining
firstprevent an already-startedsecondfrom running? - What general rule can be derived?
Key idea: Start independent work before waiting for its completion.
Counter counter = new Counter();
Thread first = new Thread(counter::increment);
Thread second = new Thread(counter::increment);
- Do both threads access the same
Counter? - Why is shared memory useful?
- Why can shared mutable state be dangerous?
- Which variables are local to each method call?
- Which fields are shared?
Key idea: Threads have separate execution stacks but can access the same objects in heap memory.
public void increment() {
value++;
}
- Is
value++necessarily one indivisible operation? - What smaller steps might it contain?
- What happens if two threads read the same value?
- Why could the final value be too low?
- Why might the error disappear in another run?
Key idea: A race condition occurs when a result depends on unpredictable timing while threads access shared mutable data.
- Why are local variables generally safer?
- Why are immutable objects useful?
- Is
ArrayListsafe for simultaneous modification? - Could each task produce an independent result?
- Why is avoiding shared state often easier than protecting it?
Key idea: Independent tasks and immutable data reduce the need for synchronization.
Thread worker = new Thread(() -> {
throw new IllegalStateException("Task failed");
});
- Which thread throws the exception?
- Does it automatically appear in the main thread?
- Does it necessarily terminate the whole application?
- How could the failure be reported?
- Why is error handling harder with multiple threads?
Key idea: An exception belongs to the thread in which it occurs. Communicating failures requires deliberate design.
- What does
Runnable.run()return? - How could the main thread receive a result from a task?
- What problems arise if tasks modify one shared result list?
- Why might Java need another abstraction for result-producing tasks?
- What roles do
CallableandFutureprovide?
Key idea: Runnable represents an action without a direct return value.
- What is the most important difference between a thread and a
Runnable? The thread is a path of execution, while theRunnabledescribes work to be done. - Why does writing a
Runnableas a lambda not make it concurrent? A lambda is just a compact way to implement a functional interface. It does not create or start a new thread. - What does
Thread.start()contribute thatRunnable.run()does not?start()creates a new path of execution that invokesrun(). Callingrun()directly uses the current thread. - Why is execution order difficult to predict? Because the operating system scheduler decides when each thread receives CPU time, and threads can be preempted at any time.
- Why does shared mutable state make concurrency harder? Because multiple threads can read and write the same data at the same time, leading to race conditions and unpredictable results.
- A thread is a path of execution.
- A Runnable describes a task.
- A lambda can implement
Runnable.run(). - Calling
run()executes the task on the current thread. - Calling
Thread.start()starts a new thread that executesrun(). - Calling
join()waits for another thread to finish. - Thread scheduling is generally unpredictable.
- Threads can share objects, which introduces the risk of race conditions.
- Independent tasks and immutable data are easier to use safely.