Skip to main content
Dat 3. semester
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Discussion points for concurrency

Classroom Discussion Points: Java Threads, Runnables and Lambdas

1. Why use multiple threads?

  • 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?

2. Processes and threads

  • 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?

3. Sequential and concurrent execution

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?

4. Concurrency and parallelism

  • 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.

5. The main thread

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?

6. Creating a thread

Thread worker = new Thread(() -> {
    System.out.println("Hello from a worker");
});

worker.start();
  • When is the Thread object 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.

7. start() compared with run()

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.

8. What is a Runnable?

Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("Performing a task");
    }
};
  • Is a Runnable a 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 Runnable execute without a new thread?

Key idea: A Runnable describes work. It does not decide where or how that work is executed.

9. Why can a Runnable use a lambda?

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?

10. Functional interfaces

Supplier () → T Consumer T → void Function<T, R> T → R Predicate T → boolean Runnable () → void

11. Anonymous class compared with lambda

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.

12. Capturing variables in lambdas

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 message be 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.

13. Capturing values in a loop

for (int i = 0; i < 5; i++) {
    int taskNumber = i;

    Thread worker = new Thread(() -> {
        System.out.println(taskNumber);
    });

    worker.start();
}
  • Why is taskNumber created 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.

14. Waiting with join()

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.

15. Start first, wait afterward

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 first prevent an already-started second from running?
  • What general rule can be derived?

Key idea: Start independent work before waiting for its completion.

16. Shared memory

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.

17. Race conditions

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.

18. Reducing shared mutable state

  • Why are local variables generally safer?
  • Why are immutable objects useful?
  • Is ArrayList safe 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.

19. Exceptions in worker threads

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.

20. Returning results

  • 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 Callable and Future provide?

Key idea: Runnable represents an action without a direct return value.

Closing discussion

  • What is the most important difference between a thread and a Runnable? The thread is a path of execution, while the Runnable describes work to be done.
  • Why does writing a Runnable as 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 that Runnable.run() does not? start() creates a new path of execution that invokes run(). Calling run() 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.

Summary

  • 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 executes run().
  • 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.