UNC COMP 301 - F26/Lec 13 - Lambda Expressions; Concurrent Programming
Watch on YouTube →
Overview
Muhammad Sayeed Ghani introduces Java lambda expressions as compact implementations of single-method interfaces, then explains concurrent programming through the distinctions among synchronous and asynchronous execution, context switching, and true parallelism. He demonstrates Runnable tasks, Thread.start() and join(), and a workload split across 1–24 threads; the timing results flatten near eight threads, matching the laptop’s reported eight available processors.
Key takeaways
- A Java lambda implements a single-method interface such as Runnable by providing the method’s parameters and body without a separate named implementation class.
- Calling Runnable.run() is synchronous, whereas calling Thread.start() schedules the task on a separate thread and lets the caller continue.
- Concurrent execution does not guarantee a particular output order: worker-thread print statements can interleave differently on each run.
- Thread.join() provides an explicit synchronization point by blocking the calling thread until the specified worker finishes.
- Parallel speedup depends on both the algorithm and hardware: divide-and-conquer tasks such as quicksort can be partitioned, while adding threads beyond available cores may cause time-slicing overhead.
- In the lecture’s benchmark, work is split across 1–24 threads, and performance flattening near eight threads aligns with Runtime.getRuntime().availableProcessors() reporting eight.
Chapters
- Ghani moves concurrent programming earlier in the course because an assignment on the topic is expected the following week.
- The lecture begins with lambda expressions, then covers computing models, programming models, threads, race conditions, and synchronization.
- Design patterns and user interfaces remain scheduled for later lectures.
- A Person-style interface with one method, getName(), can be implemented without creating a separate Student class.
- The conventional approach creates a class implementing the interface, instantiates it, and calls its method.
- A lambda gives a compact implementation when the interface has a single abstract method.
- A Java lambda has parameters in parentheses, an arrow token (->), and the method body.
- The example matches a zero-argument interface method and returns the hard-coded name Alex.
- A multi-line body uses curly braces; the declared interface type supplies the target type and identifies the method being implemented.
- The debugger displays a lambda-generated object rather than an instance of the omitted Student class.
- The example breaks baking into mixing dry ingredients, mixing wet ingredients, combining them, arranging dough, preheating the oven, and baking.
- Preheating earlier avoids idle time while ingredients are prepared.
- A faster mixer represents a hardware upgrade, while a second person represents adding a processor to perform work simultaneously.
- Sequential computing completes one task before beginning the next; older personal computers often required closing one application before opening another.
- A single processor can create the appearance of concurrency by rapidly switching among tasks, also called context switching or time slicing.
- Switching tasks has overhead: if a 10-millisecond time slice spends 3 milliseconds switching, 30% of the time is lost to overhead.
- The cookie analogy distinguishes rearranging tasks to avoid idle time from adding resources to perform tasks in parallel.
- Parallel computing runs tasks at the same time on separate processor cores, unlike time-sliced concurrency on one core.
- Synchronous method calls pause the caller until the called method returns, following the call stack.
- Asynchronous tasks let the main method continue while another task runs; this may use parallel cores or time slicing.
- Developers must decide whether work can be split safely, especially when the main method depends on a task’s result.
- Ghani describes transistor-density gains as the historical basis of Moore’s law, followed by clock-speed increases that made heat dissipation a challenge.
- From roughly 2005 onward, continued performance gains increasingly came from adding CPU cores.
- A program benefits from multiple cores only when its work can be divided into suitable tasks; unrelated background applications may use cores independently.
- Summing one million array entries can be divided into two halves, processed separately, and combined into a final sum.
- Quicksort’s divide-and-conquer structure lets separate cores sort the partitions created around a pivot.
- Merge sort is another divide-and-conquer example; selection sort and bubble sort are less directly suited to this parallel decomposition.
- Using multiple cores generally requires developers to restructure the algorithm and split its work.
- A thread is a sequence of execution that can carry out one portion of a larger task, such as a quicksort partition.
- Each thread has its own call stack and instruction pointer to track method calls and its current execution location.
- Threads typically share heap memory, making communication easier when they need access to common data.
- Java’s Runnable interface defines a task through its single run() method.
- A lambda can implement Runnable without naming a separate task class or explicitly writing the run() method name.
- The example creates a Runnable task whose loop prints the integers 0 through 9.
- Creating the Runnable object defines the task but does not execute it.
- Calling task.run() executes the Runnable synchronously on the current thread.
- Passing a Runnable to a Thread constructor and calling start() launches a separate thread of execution.
- After start(), the main thread can print “done” while the worker thread runs its loop.
- The order and interleaving of output depend on thread scheduling and cannot be reliably predicted.
- Starting two Thread objects with the same Runnable creates two worker threads in addition to the main thread.
- Their printed number sequences may interleave differently across executions, while the main thread may print “finished” before either worker completes.
- Calling thread1.join() makes the main thread wait for thread 1; a subsequent thread2.join() waits for thread 2.
- Join order matters: the main thread waits at the first join even if the second thread has already finished.
- The demonstration increases the number of worker threads from 1 to 24 and divides a workload among them.
- Each workload creates 100 million random-number-generator objects, providing a time-consuming task for comparison.
- The code records elapsed time with System.nanoTime() and joins all worker threads before completing each measurement.
- Java’s Runtime.getRuntime().availableProcessors() reports the number of processors available to the program.
- The first single-thread run takes about 47 seconds, while subsequent measurements decrease as work is distributed across more threads.
- Timing spikes can occur when operating-system interrupts or background applications compete for CPU time.
- The performance curve flattens around eight threads, suggesting the laptop has eight processors available for parallel work.
- The program confirms the estimate by reporting eight available processors through Java’s runtime API.
- Threads can use parallel hardware up to its capacity; adding threads beyond the available cores can introduce time slicing rather than improve performance.
- Ghani closes by recapping asynchronous programming, threads, and the relationship between thread count and processor capacity.
Summary, takeaways, and chapters were generated by AI from the video's transcript and may contain errors. The video belongs to its creator, Muhammad Sayeed Ghani.