Multithreading in Java
23.1 Preliminaries — Process, Context Switching and Multitasking
Hook — why do three programs look parallel on one CPU? Open a music player, a browser, and a Word document at the same time. All three seem to run together, but a single processor can hold only one program at a time. The gap between what you see and what the machine does is the entry point for threads.
23.1.1 What a Process Is
A process — an instance of a program in execution — owns its own memory area and its own data. The lecture uses three everyday examples labeled P1, P2, and P3: P1 is the music player, P2 is the web browser, and P3 is an open file such as a Word document or a spreadsheet. Each of these three counts as a distinct process because P1 has memory that belongs to the music player, P2 has memory for the browser, and P3 has memory for the file. From the user view they work together, but the machine maintains strict separation.
Formal idea — process isolation: A process is the smallest unit the operating system scheduler can dispatch when using process-based multitasking. The operating system gives each process a private address space. That means a variable address like 0x7FFF1000 in P1 and the same numeric address in P2 refer to different physical storage. No process can directly read or overwrite the memory of another without special interprocess communication, which is expensive and limited. This isolation is a safety feature built into modern operating systems.
In a single-processor illustration, imagine one CPU for the whole machine. At any moment only one process holds that CPU. At time slice 1 the music player holds the CPU, at time slice 2 the browser holds it, at time slice 3 the file process holds it. The operating system moves the CPU very fast among P1, P2, and P3. The speed is so high that the human eye cannot notice the switching in the background. The system creates the illusion that all three run together by sharing the CPU quickly. The lecture calls that fast sharing the beauty of the operating system.
The idea is operating-system related even though we learn it through Java. You need this background before threads make sense, because threads will add one more level inside a process.
Scope — when this view applies: The separate-memory model applies to processes on a conventional operating system. If the system has multiple CPUs, two processes can truly run in parallel, one per processor. On a single CPU the parallelism is an illusion created by time-slicing. The model breaks when you look inside one process — that is where threads appear and the sharing rules change.
Visual to keep in mind: draw a horizontal timeline. Mark three lanes for P1, P2, P3. Shade a short block in P1, then a short block in P2, then a short block in P3, repeating. The x-axis is time in milliseconds, the y-axis lists the processes. The shaded blocks never overlap on the single-CPU diagram. The takeaway is that at any vertical line only one block is active, but across a full second each lane gets many blocks, so the user feels continuous play, browsing, and editing.
Real-world anchor: every icon you double-click — music player, browser, Word, Excel — becomes a process with its own address space. When the system runs out of memory for a new process, creation can fail. That is why later examples wrap thread or process creation in try and catch — resources are finite and governed by operating system policy.
23.1.2 Context Switching Between Processes
Context switching — shifting the control of the processor from one process to another — is the second preliminary. With the same three processes P1, P2, and P3, the system gives the CPU from P1 to P2, later from P2 to P3, and later back to P1. Taking the CPU away from the currently running process and giving a chance to another process to run is exactly what context switching means. The lecture describes it as shifting processor control between processes.
What the switch must save and restore: A context switch between processes must save the full process context: program counter, registers, stack pointer, memory-mapping tables, and other data structures the operating system keeps for the process. It then loads the context of the next process. Because the address spaces are different, the memory management unit must be updated. That work costs time.
Assumption — cost follows isolation: Process-level switching is relatively costly because nothing is shared. The operating system must maintain process data structures and underlying fields for each process. The lecture uses this cost to motivate the cheaper thread-level switch later. If you assume all switches cost the same, you will miss why threads were invented.
Think of context switching like changing desks in an office where each person has a separate locked office with private files. To switch, you must lock your office, carry your papers out, unlock the next office, and lay out new papers. The move is safe but slow. This analogy breaks for threads, where people share one open-plan room — a picture we will extend in the next concept.
23.1.3 Multitasking and the Illusion of Parallelism
Multitasking — performing more than one task at the same time — names what the user perceives. In the lecture the term labels the situation where different processes at different memory addresses appear to run together on a single processor.
Intuition — tasks and addresses: A task here is a unit of work that lives at a distinct memory address. Because switching between P1, P2, and P3 is fast, the user perceives multiple processes running at the same time. The operating system frequently switches back and forth between processes, giving the illusion that they run in parallel. On a multi-CPU machine some of the processes can truly run in parallel, one per processor, but the perceived effect is the same.
Common pitfall to avoid now: do not equate multitasking with simultaneous use of the CPU on a single core. The CPU still serves one process at a time. The illusion comes from time slicing, not from duplication of the processor.
23.1.4 Why These Preliminaries Matter for Threads
These three ideas — process, context switching, multitasking — are presented as required background before directly learning about threads. They are largely operating system concepts, yet they prevent confusion when multithreading is introduced. The hierarchy will soon go one level deeper: from sharing a CPU among processes to sharing a CPU among parts of a single process.
Recap — the setup for threads: Processes P1, P2, P3 each own private memory; the operating system creates apparent parallelism by fast context switching between processes; multitasking is the user-visible name for that illusion. This sets up the contrast: threads will share memory inside one process and switch more cheaply. Next we open that process and divide it.
Pitfall — using the right level: Students sometimes apply process reasoning to threads, expecting isolation where there is sharing. Remember the hierarchy. Ask first: am I switching between processes or between parts of one process? The answer decides whether memory is private or shared, and whether corruption risk is low or high.
23.2 Thread and Multithreading Fundamentals
Hook — what if one program could do three jobs at once without becoming three programs? A browser loading images while you scroll, or an editor formatting text while it prints, shows the need. Doing that with a loop that does a little of each job quickly gets complex because work code and timing code get mixed. Threads separate the jobs cleanly.
23.2.1 Definition of a Thread
A thread — a programmed unit that is executed independently of other parts of the program — is the central definition. The definition is given verbatim in the lecture and then expanded with a diagram. Thread defined as independent programmed unit part of process captures the same idea: a thread is a programmed unit that is independent and is part of a process. A thread is not a separate program; it is a part inside one process that can be scheduled on its own.
Formal idea — thread as independent path: In the Java virtual machine each thread is a separate path of execution. The virtual machine executes each thread for a short time and then switches to another thread, so threads appear to run in parallel. Unlike a process, a thread does not own a private address space; it runs within the address space of its parent process. A thread terminates when its run method returns.
Plain meaning on first use: a thread here means a lightweight flow of instructions, like a lane on a single road, all lanes sharing the same road surface.
23.2.2 Multithreading as One Level Down in the Hierarchy
Take a single process P1 and divide it into multiple parts. Those parts are labeled thread number one, thread number two, thread number three, thread number four. Earlier the diagram showed different processes P1, P2, P3 sharing one processor with context switching between processes. Now the same single processor is shared between multiple parts of a single process. That is the core image of multithreading.
Intuition — two levels of sharing: Level 1 is between processes: P1, P2, P3 compete for the CPU and the operating system switches the whole process context. Level 2 is inside one process: T1, T2, T3, T4 of P1 compete for the CPU when P1 itself has the CPU. The lecture stresses: do not mix the two levels. Context switching between processes versus context switching between threads is one more level down in the hierarchy. Instead of P1, P2, P3 competing, we now talk about T1, T2 or T3, T4 or T5, T6 which are threads of those processes competing for the same single CPU when their parent process has the CPU.
The same illustration is repeated for another process P2. Threads of P2 also look to get the single CPU when P2 is given a turn. The idea transfers to any process that is partitioned.
Visual: draw the same single-CPU timeline as before but now zoom into the P1 block. Inside that block draw a finer striped pattern of T1, T2, T3, T4 alternating. The x-axis is still time, but now you see nesting: coarse switching between P1, P2, P3, and fine switching between T1 and T2 inside the P1 window. The takeaway is that thread switching happens within the process window and is much finer grained.
23.2.3 Shared Address Space and Data Structures
If a program creates multiple threads, all those threads execute in the same address space and use the same data structures. Why? Because they belong to one process that has been divided, so the data and the structures remain common. This shared address space is the key distinction from processes. Threads of P1 work on the memory that belongs to P1, threads of P2 work on memory of P2, and so on.
Formal contrast — sharing rule: Threads of a process share the same address space and cooperatively share the same heavyweight process. Interthread communication is inexpensive, and context switching from one thread to the next is lower in cost than between processes. Processes, by contrast, are isolated and cannot overwrite each other's memory by accident. The sharing is what makes thread switches fast and what makes data corruption possible if care is missing.
Analogy: processes are separate houses with private gardens — safe but each move between houses needs a full relocation. Threads are roommates in one house sharing the kitchen and living room — moving from one roommate's task to another is quick because the house stays the same, but roommates can spoil shared food if they do not coordinate. The analogy breaks when hardware adds multiple cores, where true parallel execution inside one house becomes possible.
Scope — when shared memory helps and when it hurts: Sharing helps when threads need to cooperate on one data set, such as updating one document. It hurts when two threads modify the same structure without coordination. The rule is: shared address space saves memory and switch time, but it removes the safety of isolation.
23.2.4 Intuitive Program Partitioning Example
A concrete program helps: suppose a program needs input, then it processes that input, then it prints the result on the screen or into a file. The overall input handling can be one thread, the data processing can be another thread, and the printing can be another thread. This is how a process is logically divided into threads. Each thread can be given a turn on the CPU one by one so the process executes in parts. The example makes the abstract splitting tangible: input, processing, output as three threads of the same process.
Worked partitioning — three threads for one job: Step 1, name the jobs: Thread A handles input, reading from keyboard or file. Thread B handles processing, such as computing or formatting. Thread C handles output, printing to screen or writing to a file. Step 2, allocate: all three threads live inside the same process P, so they can share the buffer that holds input data without copying it between processes. Step 3, schedule: on a single CPU the scheduler gives Thread A a slice to read, then Thread B a slice to compute, then Thread C a slice to print, cycling. If Thread A waits for user typing, Thread B can still use the CPU to process earlier input. Result: the program stays responsive because waiting in one thread does not block the whole program.
Recap — thread in one line: A thread is an independent programmed unit inside one process; multithreading is the same time-slicing idea moved one level down; threads share the parent's memory and data structures, which makes them light and fast but requires care. This prepares the direct comparison of processes and threads next.
23.3 Process versus Thread — A Direct Comparison
Hook — same CPU, two different sharing deals: If both processes and threads compete for the processor, why does the system treat them so differently? The answer changes how you reason about memory, safety, and speed.
23.3.1 Memory Sharing
For processes, there is no sharing of memory. P1 belonging to the music player, P2 to the browser, and P3 to a Word file or Excel sheet each have a different address space. No memory is shared between processes. For threads, if T1 and T2 are threads of P1, they must work on the same memory because they belong to the same process. Sharing of memory exists for threads, no sharing exists for processes. That contrast is repeated for emphasis.
Formal contrast — address space ownership: A process owns a private address space — its own memory tables, heap, and data structures. A thread has no private address space of its own; it uses the address space of its parent process. So two processes at addresses 0x1000 each refer to different storage, while two threads of one process at 0x1000 refer to the same storage. Interprocess communication is expensive and limited; interthread communication is inexpensive because no address translation is needed.
This single rule explains the other two comparison points.
23.3.2 Risk of Data Structure Corruption
Processes cannot easily corrupt each other's data structures because each has its own memory address and its own structures. Chances of corruption are negligible when there is no sharing. Threads are different. T1 and T2 of P1 work on common memory, so the chance of corrupting that memory and its structures is high if synchronization is missing. A typical hazard is described: one thread starts a change and has not yet finished, while another thread starts work on the same memory address and destroys the data. The risk is present for threads and requires careful synchronization. The warning is explicit: be very careful when using multithreading.
Warning — the lecture's careful rule: Threads share memory and so the chance of corrupting that memory and its structures is high if synchronization is missing. Picture T1 starting to update a queue — it writes the element but has not yet incremented the tail index — and T2 enters the same queue, overwrites the same slot, and increments the pointer past an empty hole. The queue now has duplicate or junk values. With processes this cannot happen by accident because P1 and P2 never share the queue storage.
A second concrete hazard from later in the lecture uses the bracket printer: two threads interleave print("[msg") and sleep and print("]") on the same shared printer buffer, producing garbled brackets. The root cause is the same: shared memory plus unsynchronized timing.
23.3.3 Cost of Context Switching
Context switching between processes is expensive because the CPU is taken entirely from one process and given entirely to another. The operating system must maintain process data structures and underlying fields, which costs time. Context switching between threads is cheaper and easier because threads share the same memory address and the same data structures within one process. The cheaper thread-level switching is a direct consequence of shared address space.
Why the cost differs: Switching processes must save and restore the full memory map, which may involve updating the memory management unit and flushing caches. Switching threads within the same process keeps the same memory map; only registers, program counter, and stack pointer change. The heavier the state to save, the longer the switch. That is why modern systems isolate processes for safety but use threads for fine-grained concurrency inside one program.
Visual: imagine two cost bars. The process-switch bar is tall, labeled with save registers + save page tables + flush cache. The thread-switch bar is short, labeled with save registers + stack pointer only. The x-axis is switch type, the y-axis is time cost. The takeaway is that for many rapid switches, thread-level switching keeps the CPU more productive.
Scope — when the rule changes: On a multi-core system, process switches can overlap with true parallel execution, but the per-switch cost remains higher than a thread switch. Also, if threads of different processes run, you still pay the process cost when switching between their parent processes. The cheap-thread claim assumes threads of the same process.
23.3.4 Summary in Words
In short, processes have separate memory, negligible cross-corruption risk, and expensive switching; threads have shared memory, higher corruption risk if unsynchronized, and cheaper switching. The comparison is operating system related, but it is stressed as essential to avoid confusion about threads.
Comparison table for fast recall:
| Dimension | Process (P1 vs P2) | Thread (T1 vs T2 of P1) |
|---|---|---|
| Address space | Private per process; no sharing | Shared within parent process |
| Communication | Expensive, needs special channels | Inexpensive, direct memory access |
| Data corruption | Negligible across processes | High if unsynchronized on shared memory |
| Context switch cost | High — full process structures saved | Low — same address space kept |
| Use case | Run separate programs (music, browser, Word) | Divide one program (input, processing, printing) |
One-sentence chooser: use processes to isolate whole programs; use threads to cooperate quickly inside one program and protect shared parts with synchronization.
Recap and bridge: The three-way contrast — memory, safety, and speed — explains why threads are light but need careful coordination. With that mental model fixed, the next concept adds timing: how a thread lives, waits, and dies so you can give each thread a fair turn.
23.4 Thread Life Cycle and States
Hook — how does the scheduler avoid letting one long job starve the others? If T1 runs until it finishes, T2 and T3 wait idle. The answer is planned waiting: give each thread states it can move between so the CPU never sits still.
23.4.1 Overview of States
A thread moves through several states during its lifetime. The lecture names the states and describes how a thread switches between them based on program conditions and planning. The states discussed are New, Runnable, Running, Blocked or Waiting, Timed Waiting, and Terminated or Dead. This life cycle is used to explain how multiple threads can be coordinated even when one thread needs a long time.
The state set in one view: Think of a thread as always in one of these buckets: New means created but not yet started; Runnable means ready and waiting for the scheduler to give it the CPU; Running means actually executing its run method; Blocked or Waiting means paused until some event occurs; Timed Waiting means sleeping for a fixed duration such as 1000 milliseconds; Terminated or Dead means the run method has finished and the thread will not run again. The scheduler moves threads between these buckets.
23.4.2 New, Runnable and Running
A new thread is one that has been created but not yet started. Once the start method is invoked the thread becomes Runnable, meaning it is ready to run and waiting for CPU allocation. When the scheduler gives it the CPU it enters the Running state and executes its run method. The lecture focuses more on the waiting and terminated states because they are used to control execution order, but the New to Runnable to Running progression is implied in every example where start leads to run.
Intuition — ready versus running: Runnable does not mean running. A runnable thread is in the ready queue; the scheduler picks among runnable threads, often by priority, and only the chosen one becomes running. When its time slice ends or it blocks, it leaves running and may return to runnable. This distinction explains why creating five threads does not guarantee they run in creation order; they all become runnable together and the scheduler decides.
Pitfall — start versus run: Call start to move from New to Runnable. If you call run directly, you stay in the current thread and no new thread enters the life cycle. The lecture's later examples always use obj.start() for this reason. Calling run as a normal method is a frequent beginner error that produces no concurrency.
23.4.3 Blocked, Waiting and Timed Waiting — Sleep Example
If only T1 is given a chance, T2 and T3 remain inactive until T1 finishes. To avoid that stall, a planned waiting period is introduced. A detailed scenario is given: put T1 into sleep mode for 1000 milliseconds. That is a planned waiting. During those 1000 milliseconds when T1 sleeps, chance should be given to T2. After some time T2 should also go to sleep for 500 milliseconds, then chance goes to T3. With this planning, all threads get at least one chance without waiting for another thread to complete fully. This is called the timed waiting state that is planned in advance so the thread can be put to sleep for a decided amount of time.
The description distinguishes a general blocked state — the thread is running but waiting for some event or temporary condition — and the specific timed waiting where sleep duration is fixed. Examples later use sleep to demonstrate the state in code.
Timed waiting scenario with numbers: Start with T1, T2, T3 all runnable. Step 1: scheduler picks T1, T1 runs briefly then calls Thread.sleep(1000). T1 moves to Timed Waiting for 1000 ms. Step 2: scheduler sees T2 and T3 runnable and picks T2. T2 runs, then calls Thread.sleep(500). T2 moves to Timed Waiting for 500 ms. Step 3: only T3 remains runnable, so T3 runs. After 500 ms T2's sleep ends, so T2 becomes runnable again and can be picked. After 1000 ms T1's sleep ends, so T1 rejoins the runnable set. Result: no thread monopolizes the CPU; planned sleeps interleave execution rather than keeping the CPU idle.
The same idea is repeated with different numbers: T1 sleep 1000 milliseconds, T2 sleep 500 milliseconds, and the interleaving consequence is explained. When T1 sleeps, T2 runs; when T2 sleeps, T3 or the next waiting thread runs. This planned sleep avoids keeping the CPU idle unwatched and lets other threads progress. The lecture notes that when a sleeping thread wakes, it does not automatically resume running — it becomes runnable and must be picked again by the scheduler.
Visual: draw a state diagram with nodes New, Runnable, Running, Blocked/Waiting, Timed Waiting, Terminated. Arrows: start from New to Runnable; scheduler pick from Runnable to Running; sleep or wait from Running to Timed Waiting or Blocked; timeout or notify from those back to Runnable; run return from Running to Terminated. The takeaway is that only Running uses the CPU, while the waiting states explicitly free it.
Assumption — timed waiting needs handling: Thread.sleep throws InterruptedException, a checked exception. You must surround it with try and catch or declare it. The lecture wraps sleep in try and catch in every example because a sleeping thread can be interrupted and should handle the event rather than end abruptly.
23.4.4 Terminated or Dead State and State Transitions
The last state is Terminated, also called Dead. As the name suggests, the thread is dead and no longer needed after it has done its functioning. It has moved to the dead state. The lecture presents the life cycle as a set of switches between these states driven by program conditions and whatever planning the developer has done. State transitions are not automatic alone; they respond to sleep calls, waiting for events, and completion of the run method.
How death occurs: A thread terminates when its run method returns, either by reaching the end or by throwing an uncaught exception. The lecture states: a thread is alive as long as its run method executes. Once run exits, the thread moves to Terminated or Dead and cannot be restarted. Calling start again on a terminated thread throws IllegalThreadStateException. If you need the same work again, create a new thread object.
Recap — the whole cycle in one line: New is created but not started, start makes it Runnable, the scheduler moves it to Running, sleep or waiting moves it to Timed Waiting or Blocked, waking moves it back to Runnable, and run finishing moves it to Terminated. Bridge: with states clear, the next section shows how Java lets you actually create threads using the Thread class and the Runnable interface.
23.5 Creating Threads in Java — Thread Class and Runnable Interface
Hook — why does Java offer two ways to make one thread? Both ultimately provide a run method, but the design choice affects what your class can inherit and how you name or prioritize the thread. Understanding both paths keeps you flexible.
23.5.1 Two Ways to Create a Thread
There are two ways to create threads in Java. The first way is by extending the Thread class. The second way is by implementing the Runnable interface. Both are predefined: Thread is a predefined class available with Java, Runnable is a predefined interface. Either path lets a program create a thread. How to do each is shown through examples later. This two-path framing is repeated for retention.
Two paths, one entry point: In both paths the thread's work lives in public void run(). Path 1: create a class ABC extends Thread, override run, then ABC a = new ABC(); a.start();. Path 2: create a class ABC implements Runnable, define run, then Thread t = new Thread(a); t.start();. Both end at run when the scheduler gives the new thread the CPU.
23.5.2 Thread Class — Constructors, Fields and Methods
The Thread class provides constructors and methods to create and perform operations on a thread. As a class it supplies constructors and methods designed for thread work.
Four constructors are listed:
- No argument constructor —
new Thread(). - Single argument constructor where we receive the name of the thread as a String —
new Thread("MyThread"). - Single argument constructor where we receive an object of the Runnable interface which is initialized taking a reference of the class on which it is implemented —
new Thread(runnableObj). - Two argument constructor taking the name of the thread as well as the Runnable object —
new Thread(runnableObj, "MyThread").
Any of these constructors can be used to create an object of the Thread class. The lecture demonstrates each of these forms in later examples.
Data members of the Thread class include two fields:
- Name of the thread — the first field.
- Priority of the thread — the second field.
Priority is order of execution. An example is given: T1 has priority 2, T2 has priority 6. Six is higher than two, so T2 has higher priority and should be considered first when allocating the CPU. Once T2 has done its execution or moved to sleep mode, chance is given to T1. The default priority for a newly created thread is 5, and it can be changed.
Methods available with the Thread class that are named include:
getNameandsetNamefor getting and setting the name of the thread.getPriorityandsetPriorityfor priority.isAlivewhich returns true if the thread is alive.currentThreadwhich returns the current active thread when multiple threads exist. It is apublic staticmethod called asThread.currentThread().activeCountwhich returns the total number of threads currently active.
In the examples these constructors and methods are used to observe functioning. Line-by-line demos of each are promised and then delivered.
Scope — what priority can and cannot guarantee: Priority values range from MIN_PRIORITY 1 to MAX_PRIORITY 10, with NORM_PRIORITY 5 as default. Higher priority suggests the scheduler should prefer that thread, but actual selection also depends on underlying system resources and operating system policies. On some platforms equal-priority threads are time-sliced automatically; on others they must voluntarily yield. For portable behavior, do not rely on priority alone — combine it with sleep, yield, or proper waiting.
Visual for constructors: picture four factory doors labeled no-arg, name-only, runnable-only, runnable+name. Each door outputs a Thread object, but only the doors that receive a Runnable carry custom work; the no-arg door outputs a thread with empty run.
23.5.3 Runnable Interface and the run Method
The Runnable interface should be implemented by any class whose instances are intended to be executed by a thread. As an interface it means method declarations. The key fact is that any class implementing Runnable must define one method: public void run. The name is repeated: public void run. Thread execution starts from public void run. A class may have other methods, but having public void run is necessary because whatever task the thread must perform should be available in public void run and must be defined in the program.
Interface detail — single method contract: Runnable contains only one method named public void run. It is used to perform action for a thread. The task of a thread is described by the instructions in the run method. A thread is alive as long as its run method executes. When run returns, the thread dies. This matches the life-cycle rule introduced earlier.
Pitfall — forgetting the signature: The method must be exactly public void run() with no arguments and no return value. Writing public void run(String s) or making it private means you have not satisfied the interface, and the thread will run the empty base version instead of your work.
23.5.4 Relationship — Thread Implements Runnable
A point presented as surprising: the Thread class itself implements the Runnable interface. So there are two routes that converge. If a class ABC extends Thread, that is the first way of creating threads, it indirectly goes through Runnable because Thread implements Runnable, and that is why public void run is available there. Alternatively, a class can directly implement Runnable and provide public void run. Both routes lead to defining run for the thread task. This duality is explained twice for clarity.
Intuition — why two routes exist: Extending Thread is convenient when you need Thread's other methods and you do not need to inherit from another class. Implementing Runnable is more flexible when your class already extends another class, because Java allows only one superclass. The lecture recommends Runnable when you will not override other Thread methods. In both cases the scheduler enters through the same door: the run of the Runnable object supplied to the Thread, or the overridden run of the Thread subclass itself.
23.5.5 Thread Priority — Default and Customization
Priority was already introduced as a data member. The elaboration covers default value 5, how to change it, and how to read it back. The methods setPriority and getPriority are paired with setName and getName. The lecture notes that changing priority affects scheduling order but the actual selection also depends on underlying system resources and operating system policies. A later worked example shows changing main thread priority from 5 to 2 and printing the values before and after.
Quick priority change sketch: Thread t = Thread.currentThread(); now t.getPriority() returns 5. Call t.setPriority(2); then t.getPriority() returns 2. If T1 has priority 2 and T2 has priority 6, the scheduler should prefer T2, giving it CPU before T1, and will give T1 a turn when T2 sleeps or blocks. The numbers themselves do not make a thread run faster; they only influence who gets picked next.
23.5.6 Exception Handling Around Thread Operations
When creating multiple threads, the surrounding code in several examples wraps thread work in try and catch blocks. The reason given is that based on available resources on the system or operating system policies, an attempt to create multiple threads may not be allowed, or sufficient memory or resources may be unavailable. In that case an exception may occur. To avoid abnormal termination and runtime exceptions, the statements that get the current thread or id or other thread operations are placed in try and catch blocks. This practice is presented as part of multi-threaded programs, not as an afterthought.
Pitfall — ignoring InterruptedException: sleep, join, and wait throw InterruptedException, a checked exception. You must handle it. Squelching it with an empty catch that ignores interruption can hide a request to stop. The accepted pattern is to catch and either clean up and exit or re-set the interrupted flag with Thread.currentThread().interrupt() so callers see the signal.
Recap — the creation toolkit: Thread offers four constructors, two fields (name, priority with default 5), and methods getName, setName, getPriority, setPriority, isAlive, currentThread, activeCount; Runnable requires exactly public void run; Thread itself implements Runnable, so both paths converge on run; priority nudges scheduling but does not guarantee order; and thread operations that may block must be handled with try and catch.
23.6 Worked Examples of Single Thread Creation
Hook — can you make a thread without any custom work? Java says yes, but the result shows why run matters: a thread without run does nothing, while one with run carries its task.
23.6.1 Example 1 — Extending Thread with Public Void Run
This first example talks about creating a thread using the Thread class.
Code — extend Thread:
class ABC extends Thread {
public void run() {
// task for the thread
System.out.println("Task inside run");
}
}
public class Main {
public static void main(String[] args) {
ABC a = new ABC();
a.start();
}
}
Step-by-step execution: 1. ABC a = new ABC(); creates a new thread object in state New. 2. a.start(); is a predefined method of Thread; it moves the thread from New to Runnable and asks the scheduler to pick it. 3. When the scheduler gives it the CPU, control enters public void run() of ABC. Whatever was written inside run now executes in the new thread, not in main. No name or priority was set, so defaults apply (name like Thread-0, priority 5). Key rule: call start, not run, to get a new path of execution.
This pattern is presented as the equivalent of example number one in the slides, extending Thread and defining run and starting with start.
Pitfall — start can be called once: After run finishes, the thread is Terminated. Calling start again throws IllegalThreadStateException. If you need the same task again, create a new ABC object.
23.6.2 Example 2 — Direct Thread Object Without Extending
This second example uses the Thread class again but with a different constructor where the name of the thread is not explicitly given, and no subclass is created.
Code — plain Thread object:
public class Main {
public static void main(String[] args) {
Thread t = new Thread();
t.start();
System.out.println(t.getName());
}
}
What happens: Up to main, inside main an object t of the Thread class is created with the no-argument constructor and t.start() is invoked. In the previous example an object of ABC was created and started via a.start(). Here no subclass of Thread is created; instead an object of the Thread class itself is created. Because public void run was not overridden and no Runnable was supplied, run does nothing useful. The thread is still created because an object of Thread was used and start was invoked. It moves from New to Runnable to Running and then immediately to Terminated, doing nothing because run was not defined. Calling t.getName() and printing it shows the default name and default values the JVM assigned, such as Thread-0. This demonstrates that a thread can be created even without extends or implements, though it performs no custom task.
Why it matters: it proves that thread creation is tied to the Thread object and start, while useful work is tied to run. No run means no work, but still a lifecycle was traversed.
23.6.3 Example 3 — Implementing Runnable and Passing to Thread
Previous two examples were related to the Thread class; this one is related to the Runnable interface.
Code — implement Runnable:
class ABC implements Runnable {
public void run() {
System.out.println("Task from Runnable");
}
}
public class Main {
public static void main(String[] args) {
ABC a = new ABC();
Thread t1 = new Thread(a);
t1.start();
}
}
Step-by-step: 1. class ABC implements Runnable must define public void run(). 2. In main, ABC a = new ABC(); creates the task object. 3. Thread t1 = new Thread(a); uses the third constructor of Thread which takes a Runnable object. The note adds that Runnable a = new ABC(); is also valid, taking a reference of the implementing class. 4. The moment a is passed to Thread and t1.start() is invoked, control is taken to the body of ABC, looking for run, and content inside run starts executing. Within main the Thread object is initialized with the object of the class that implements Runnable, and start takes control to public void run by default. Whatever was written inside gets executed. That is the hierarchy of thread execution for Runnable.
The Runnable path keeps class ABC free to extend another class if needed, because it does not extend Thread.
23.6.4 Example 4 — Runnable Object Plus Thread Name Using the Fourth Constructor
This example creates a Thread object using the fourth constructor which takes both a Runnable object and a String name.
Code — Runnable plus name:
class ABC implements Runnable {
public void run() {
System.out.println("Task from Runnable with name");
}
}
public class Main {
public static void main(String[] args) {
ABC a1 = new ABC();
Thread th1 = new Thread(a1, "MyThread");
th1.start();
String str = th1.getName();
System.out.println(str);
}
}
Step-by-step: class ABC implements Runnable defines run. In main create a1 of ABC. Then create Thread th1 with two arguments: first is the Runnable object a1, second is the name MyThread. The name could be any string supplied as second argument. After start, control goes to the run method of ABC and whatever was written there executes. Then th1.getName() is called to retrieve the name and System.out.println prints it. Expected output includes MyThread on one line (plus whatever run printed). This shows how the two-argument constructor simultaneously associates the thread with its task class and assigns its name, avoiding a separate setName call.
23.6.5 Example 5 — The Main Thread and Changing Name and Priority
This example is about changing thread name and priority and is described as very interesting, explained line by line with output.
Code — inspect and alter the main thread:
class Test1 {
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println("Current thread: " + t);
System.out.println("Name: " + t.getName());
System.out.println("Priority: " + t.getPriority());
t.setName("MyThread");
System.out.println("After name changed: " + t.getName());
t.setPriority(2);
System.out.println("After priority changed: " + t.getPriority());
System.out.println("After change details: " + t);
}
}
Line-by-line walk with system output: When a program starts, a thread is automatically created called the main thread — an automatic single thread of execution present from the start. At Thread t = Thread.currentThread() no user thread has been created yet, so currentThread() returns the main thread.
Printing t with System.out.println("Current thread: " + t) on the system shown prints Thread[main,5,main]. That breaks into three parts: name main, priority 5, and thread group main. So t.getName() prints main, and t.getPriority() prints 5, which is the default NORM_PRIORITY.
Changing name: t.setName("MyThread") changes default name main to MyThread. A subsequent getName() prints MyThread, shown as After name changed MyThread.
Changing priority: t.setPriority(2) changes default 5 to 2. A subsequent getPriority() and printed details show priority 2. After change the details reflect Thread[MyThread,2,main] or similar, confirming both fields were altered.
Bolded outcomes: Initial print is Thread[main,5,main]. After setName the name is MyThread. After setPriority(2) the priority is 2 and the final thread string is Thread[MyThread,2,main].
Pitfall — priority range: setPriority only accepts 1 to 10. Passing a value outside this range throws IllegalArgumentException. Stick to the constants MIN_PRIORITY, NORM_PRIORITY, MAX_PRIORITY when you need fixed levels.
Concepts reinforced across all five single-thread examples: Extending Thread with public void run and starting via a.start is Example 1; a direct Thread object without extending shows defaults in Example 2; implementing Runnable and passing to Thread via the Runnable constructor is Example 3; adding a name via the fourth constructor is Example 4; and inspecting and changing the automatic main thread's name and priority via currentThread, setName, setPriority, getName, getPriority is Example 5.
Recap — single-thread toolkit in one line: Choose extends Thread or implements Runnable to supply run, use one of the four Thread constructors to name or attach the task, start with start, and inspect the always-present main thread with currentThread. Bridge: now we loop this creation to make many threads run together and learn how to mix sleep and join to control them.
23.7 Multithreading in Practice — Creating and Controlling Multiple Threads
Hook — what changes when five threads print at once? Ids appear out of order and lines interleave. That nondeterminism is not a bug but the scheduler in action — and sleep and join are the tools to shape it.
23.7.1 What Multithreading Means and Why Shared Memory Matters
Multithreading is defined as a process of executing multiple threads simultaneously. Recall P1 with threads T1, T2, T3 sharing the memory address available to P1. That memory address is shared between all threads. After some time a thread gets a chance to execute in the Running state and may go to a temporary suspended Blocked or Waiting state. For example, first T1 is given a chance, then after some reason T1 must wait so its execution is stopped but not completed, and chance is given to T2. When T1 stops and returns, it resumes from where it stopped, but during that time contents of memory may have been changed by T2. Whenever multiple threads execute simultaneously, side effects are possible and awareness is needed, along with methods to deal with them.
This section is framed as three subparts: first learn multi-threading and how to create and execute multiple threads and see output, then look at consequences of having multiple threads, then how to resolve them.
Additional points restated: When a program starts a thread is automatically created called the main thread, threads use a shared memory area covering the heap and many structures, they do not allocate a separate memory area per thread and so save memory, and context switching between threads takes less time than between processes. The saving and speed are due to shared area. A common template for upcoming examples is class Test1 extends Thread or a class that uses Runnable with run wrapped in try and catch for resource reasons, and main creating multiple threads.
Scope — side effects from shared memory: Because T1 and T2 share the same heap, a pause in T1 followed by a write in T2 can change what T1 sees when it resumes. That is exactly the corruption risk flagged in the process-versus-thread comparison. Later synchronization will fix it, but first you must see the interleaving without it.
23.7.2 Creating Five Threads with a Loop Using the Thread Class
This example corresponds to example numbers five and six in the slides that create five threads using the Thread class.
Code — five threads via Thread subclass:
class Test1 extends Thread {
public void run() {
try {
System.out.println("Current thread: " + Thread.currentThread()
+ " ID: " + Thread.currentThread().getId());
} catch (Exception e) { }
}
}
public class Driver {
public static void main(String[] args) {
int n = 5;
for (int i = 0; i < n; i++) {
Test1 obj = new Test1();
obj.start();
}
}
}
Explanation: Test1 extends Thread and provides public void run. Within run the code prints current thread and its id and is placed in try and catch to handle the case where the operating system disallows thread creation due to resources. main sets n = 5 for five threads. A for loop with i from 0 while i < n with i++ creates an object of Test1 and invokes obj.start() each iteration. This is the same as the very first single-thread example but repeated. start creates a thread and takes control to public void run. The statement is inside the loop so it executes five times. Each execution creates a new thread and goes to run again, repeating for five threads. No name, id, or priority is supplied; defaults will be given by the JVM in coordination with the operating system.
Observed output on the lecture system: lines such as Current thread Thread[Thread-0,5,main] ID: 14 etc., with ids 14, 18, 17, 16, 15 in non-sorted order. The sequence 14, 18, 17, 16, 15 is not in creation order. The reason is internal context switching: if thread 14 takes a while and is put to wait, the scheduler gives chance to 15, but 15 may still be waiting, so it gives chance to 16 and so on, eventually 18 executes first then backtracks through 17, 16, 15 depending on which thread is in Blocked versus Runnable versus Running. This switching happens at very high speed, so the user only sees the final printed order. Different numbers may appear on a different system based on operating system policies or JVM resource handling, because thread id allocation is up to the JVM and not fixed by specification.
Hands-on check: the lecture advises to execute the program on your own system and observe the sequence you get, noting that ids and order vary across runs and machines. Variation itself is the lesson.
23.7.3 Creating Five Threads with a Loop Using Runnable
The same five-thread creation is now done by implementing Runnable instead of extending Thread.
Code — five threads via Runnable:
class Test2 implements Runnable {
public void run() {
try {
System.out.println("Current thread: " + Thread.currentThread()
+ " ID: " + Thread.currentThread().getId());
} catch (Exception e) { }
}
}
public class Driver2 {
public static void main(String[] args) {
int n = 5;
for (int i = 0; i < n; i++) {
Thread obj = new Thread(new Test2());
obj.start();
// alternative: Runnable r = new Test2(); Thread obj = new Thread(r);
}
}
}
Explanation: The first way used extends Thread; the second way uses implements Runnable. Here Test2 implements Runnable, run method remains the same printing current thread and id in try catch. In main still five threads are wanted. Because the class implements Runnable, an object of the Thread class is created taking a reference of Test2. Writing new Thread(new Test2()) or creating Runnable r = new Test2() then new Thread(r) are both valid; the Thread must be referenced to the class in which it should execute. Then obj.start() is invoked each loop iteration, five times, five threads are created, each start takes control to public void run where the print executes. Output sequence is similar: Thread 14, 18, 17, 16, 15 with the same nondeterministic ordering reason due to JVM id allocation and scheduler context switching. Trying both the Thread class and Runnable versions is advised.
Note: The lecture mentions a planned program to create two threads with factorial functionality as example number seven, but the lecture had already run over an hour and a short break of seven to eight minutes was taken, so that factorial example's detailed walkthrough is not included beyond its mention.
Pitfall — sharing the same Runnable instance: If you pass the same Runnable object to multiple Threads and that object holds mutable fields, all threads share those fields. That can add a hidden sharing hazard on top of the normal shared heap. Give each thread its own Runnable instance when the task holds state, or guard shared state with synchronization.
23.7.4 Controlling Execution with Sleep — Timed Waiting
The sleep method is used to put a thread into a waiting state for a decided temporary time, implementing timed waiting.
Detailed sleep interleaving — child 1000 ms, main 500 ms:
A class named IntThread implements Runnable and has fields like String name and Thread t. Within its constructor, t is initialized with the current invoking object and a name TestThread, it prints child thread and invokes t.start(). When t.start() is invoked it looks for public void run because Runnable is implemented. Inside run a for loop prints Child Thread 5, 4, 3, 2, 1 because i is initialized with 5 and decrements, and Thread.sleep(1000) is invoked. That means the first instance of the thread gets a chance and then goes to sleep for 1000 milliseconds. Once it wakes up, chance is given again. Sleep puts the thread into waiting state for a temporary time.
In main, an object of IntThread is created via its constructor. Inside that constructor the thread was initialized and started. In main itself, within a try block, the main thread also executes for five times with sleep 500 milliseconds. For sense: main thread sleeps 500 milliseconds, child thread sleeps 1000 milliseconds. Execution always starts from main. Main executes first then sleeps 500 milliseconds, chance goes to child, child executes and sleeps 1000 milliseconds, in the meantime main wakes up.
Timeline with numbers: Time 0 ms: main prints 5 and sleeps 500. Time ~10 ms: child prints 5 and sleeps 1000. Time 500 ms: main wakes, prints 4, sleeps 500. Time 1000 ms: child wakes, but main already woke at 500 and again at 1000, so around this region you see two main prints for one child print. The lecture writes expected interleaving as main, child, main, main, child, main, main, child then main finishes its five executions and the remaining child iterations run because the processor is free. More precisely the observed sequence shown is child, main, child, main, main, child, main, main, child for the main phase, then main completes and two more child iterations finish before exit. The point is that when child sleeps for 1000 milliseconds, two instances of main can execute in between. Sense-check: a shorter sleep (500) fits about twice into a longer sleep (1000), so the faster sleeper gets roughly double the turns — exactly what the output shows.
This demonstrates timed waiting and how sleep lets other threads get CPU instead of keeping it idle while one thread would otherwise monopolize it. Note that sleep is static: Thread.sleep(500) always sleeps the currently running thread, regardless of which object you call it on.
Exception handling rule: sleep throws InterruptedException. Wrap it in try and catch. If you catch and ignore the interruption, a request to stop the thread is lost. The better pattern is to either exit the loop or re-interrupt: Thread.currentThread().interrupt();.
23.7.5 Controlling Execution with Join — Waiting for Completion
The join method is used to wait for the current thread to complete its execution. sleep puts a thread to sleep for a set time; join makes the calling thread wait until the target thread finishes.
Join pattern — main waits for workers: A class Test1 implements Runnable has data members String name and Thread t. Its constructor takes a string, creates a new Thread(this, name), prints the new thread, and invokes t.start(). That start looks for public void run. run contains a for loop iterating five times with Thread.sleep(500) each time.
Driver class Test2 in public static void main creates t1 and t2 as new Test1("One") and new Test1("Two"). It first checks whether t1.t and t2.t are alive via isAlive, and if alive it invokes t1.t.join() and t2.t.join(). This means wait for the threads to execute as well. The construction path is: creating an object goes to constructor, from constructor to run where the thread starts, so t1 starts first then t2 starts, alive status printed, then join called for both. The effect is that the main thread will wait at join and not exit before t1 and t2 complete. After join returns, isAlive becomes false, confirming the workers finished.
Contrast in one line: sleep(500) means wait 500 ms regardless of others; join() means wait until that specific thread terminates, no matter how long it takes.
23.7.6 Combining Sleep and Join — Coordinated Interleaving
A combined example puts both concepts together. It has the same Test1 with run sleeping 500 milliseconds each iteration and driver Test2 creating t1 and t2, printing alive status then invoking t1.t.join() and t2.t.join(). Because both threads go to the same run with sleep(500), the behavior is: t1 executes and sleeps 500 milliseconds, chance is given to t2, t2 sleeps 500 milliseconds, chance back to t1, and the cycle continues despite the join waiting and keeping the scheduler active. The lecture notes that when a thread goes to sleep the CPU cannot be kept idle, so chance must be given to another thread. The interleaving is shown in output: Waiting for threads to finish then prints alternating lines from both workers and finally One exiting and Two exiting, indicating alternating execution rather than strict sequential blocking. After both finish, isAlive returns false, printed as not alive. This demonstrates coordination: sleep creates timed waiting, join ensures waiting for completion, and together they show how threads interleave when given chances.
Practical guidance: change the sleep timer and join usage to better understand how threads work and how the operating system responds. That hands-on experimentation is highly recommended.
Scope — join does not lock memory: join only coordinates completion; it does not make shared data safe while threads are still running. Use join to decide when results are ready, and use synchronization to decide when shared structures can be touched.
23.7.7 Observations on Thread IDs and Nondeterministic Ordering
Both five-thread examples yielded ids 14, 15, 16, 17, 18 but in order 14, 18, 17, 16, 15. The lecture stresses that this sequence is not fixed and may be different on the next execution or on another machine due to JVM policies and underlying resources. Thread id allocation is up to the JVM, not the specification. Context switching decisions depend on which thread is in Blocked or Running, and the operating system performs switching at high speed invisible to the user. Only the final printed order is seen. Students are advised to run the programs themselves to see their own sequences and ids.
Recap — controlling many threads: Creating five threads in a loop shows nondeterministic scheduling and JVM-assigned ids; Thread.sleep implements timed waiting that frees the CPU for runnable peers; Thread.join with isAlive lets one thread wait for another to terminate; combining them shows coordinated interleaving where sleep drives fairness and join drives completion. Exam note: be ready to predict output order and to explain why ids and order vary and how changing sleep durations reshapes the interleaving.
23.8 Thread Synchronization — Monitors, Synchronized Methods and Blocks
Hook — why do correct prints become garbled when you add threads? Three threads sharing one printer routine should each produce [Hello]. Without coordination they print Hello [Synchronized [World] ] ]. The gap is not logic but timing — and synchronization fixes timing.
23.8.1 The Problem — Corruption of the Shared Resource Without Synchronization
Even after controlling priority, sleep, and join, desired tasks may still fail because shared memory or data structures can be corrupted. When multiple threads are created and executed simultaneously but side effects are not handled, synchronization is needed. There are predefined methods, statements, and blocks available to better control thread functioning. This is described as one of the important and interesting topics, with several examples.
The underlying risk was already introduced: threads share memory, so unsynchronized access to a common area can corrupt data. Synchronization addresses that.
Scope — when you must synchronize: Synchronize whenever two or more threads need access to a shared resource that has more than one step, such as check-then-act or print-open-then-sleep-then-print-close. A single atomic read or write may need less, but any multi-step sequence on shared state is a candidate for corruption. The examples below use a shared CallMe object with bracket printing to make the window visible.
23.8.2 Monitor and Mutually Exclusive Block Concept
Synchronization is introduced when two or more threads need access to a shared resource and must be synchronized. The key to synchronization is the concept of the monitor — an object that is used as a mutually exclusive block.
Visualization: Suppose thread T1 executes some task, T2 executes some task, and there is some common area between them with common code. After executing that common area, each continues. That common area is called a mutually exclusive block. Why mutually exclusive? Because at a time if T1 is executing then T2 should not be able to enter, or if T2 is executing then T1 should not be able to enter. Only one thread should execute that block at a time.
Control is via a monitor, described like a token to access the mutually exclusive block. First T1 is given grant to the monitor, so when T2 comes and sees T1 holding the monitor it cannot enter. Similarly if T2 holds the monitor T1 cannot enter. Only one thread can own a monitor at a given time because it is like a token for the mutually exclusive block, acquiring a lock that suspends all other threads attempting to enter until the first thread exits the monitor. The monitor locks other threads until exit.
Method-level rule: To enter an object's monitor just call a method that has been modified with the synchronized keyword. While a thread is inside a synchronized method, all other threads that try to call it on the same instance have to wait. When a thread exits the monitor it simply returns from the method and the monitor becomes available for other threads. This definition of synchronized method is the first synchronization mechanism.
Formal monitor rule: Every Java object has an implicit monitor. Acquiring the monitor is entering a synchronized method on that object or a synchronized(object) block. Only the owner can execute inside; others block until the owner releases by returning or exiting the block. A thread that owns a monitor can reenter the same monitor. This matches the telephone-booth picture where only one person can be inside with the door closed, and wait temporarily steps outside.
Telephone-booth analogy from the companion book: the object is a booth, threads are people wanting to make a call. Only one person can be inside. If the coin reservoir is full, the person inside must wait outside to let a technician in — that maps to wait and notify.
23.8.3 Worked Demonstration Without Synchronization — CallMe Caller Synch
Code without synchronization is shown to make the problem visible.
Shared code — no synchronization:
class CallMe {
void call(String msg) {
System.out.print("[" + msg);
try { Thread.sleep(1000); } catch (Exception e) {}
System.out.println("]");
}
}
class Caller implements Runnable {
String msg;
CallMe target;
Thread t;
Caller(CallMe targ, String s) {
target = targ;
msg = s;
t = new Thread(this);
t.start();
}
public void run() {
target.call(msg);
}
}
class Synch {
public static void main(String[] args) {
CallMe target = new CallMe();
Caller ob1 = new Caller(target, "Hello");
Caller ob2 = new Caller(target, "Java");
Caller ob3 = new Caller(target, "Programming");
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch (Exception e) {}
}
}
Intended behavior: Passing "Hello" should print [Hello] as a unit — open bracket, message, sleep 1000 milliseconds, then close bracket — then the next message as [Java] and so on.
Actual interleaved execution without synchronization: Step 1: ob1 enters call("Hello"), prints [Hello, then sleeps 1000 ms. Step 2: during that sleep the running thread is in Timed Waiting, so the CPU cannot stay idle and the scheduler picks ob2. ob2 enters the same call on the same target and prints [Java. Step 3: ob2 sleeps 1000 ms; scheduler picks ob3, which prints [Programming. When sleeps end, the closing brackets ] appear in wake order, producing garbled output such as [Hello [Programming [Java ] ] ] or similar mixed lines. In the textbook run the output appears as mixed brackets: Hello [Synchronized [World] ] ] in spirit, with opening and closing brackets no longer paired. The shared target was the critical section call, and all three thread objects ob1, ob2, ob3 executing it need to be synchronized so that open bracket, message, and close bracket stay together before the next thread starts. Result: concurrency without mutual exclusion preserves liveness but destroys correctness.
Pitfall — sleep inside a critical section reveals the race: The sleep(1000) is intentionally placed between the two prints to widen the window where interleaving can occur. Without sleep the same race still exists but is harder to observe because the window is narrow. Do not assume code without sleep is safe — it simply hides the race.
23.8.4 Solution 1 — Synchronized Method
To avoid the problem, use the synchronized method. Put the synchronized keyword before the definition of the call method.
Fix — synchronized method:
class CallMe {
synchronized void call(String msg) {
System.out.print("[" + msg);
try { Thread.sleep(1000); } catch (Exception e) {}
System.out.println("]");
}
}
What happens now: If a thread is executing the call method, during that duration even if it sleeps 1000 milliseconds, other threads cannot enter; they must wait until execution of ob1 finishes, then they can enter this call method because of the synchronized keyword. The keyword assigns rules so execution of call is not interrupted, though it deliberately keeps the monitor locked while sleeping — the thread stays owning the monitor even in Timed Waiting, so no second thread can enter the method until the first exits.
Effect on output: Desired output after this fix is one complete unit after another: [Hello] then close, then [Programming] then close, then [Java] then close (order among the three may be Hello, Programming, Java or Hello, Java, Programming depending on scheduling, but each unit is atomic). The functioning is affected because output comes slightly later one after another, but the sequence is not disturbed and appears the usual way it should. The importance of the predefined synchronized keyword is stressed: it is already available with the language and should be used to make call mutually exclusive. If object one is executing, object two cannot enter until finish.
Visual after the fix: the same timeline now shows bracket blocks that never overlap. Each block [msg] occupies a solid interval on the target lane, with sleep inside the block. The x-axis is time, the y-axis lists threads; the target monitor line shows only one thread owning it at a time.
23.8.5 Solution 2 — Synchronized Block or Statement
The second way is simple: put the calls to the method inside a synchronized block, also called the synchronized statement. If you do not want the whole method to be synchronized, do not put synchronized before the method but just wrap the call to the mutually exclusive method within a synchronized block.
Fix — synchronized block in the caller:
public void run() {
synchronized(target) {
target.call(msg);
}
}
Here synchronized(target) is the block header where target is the object of the CallMe class, and within the block the statement target.call(msg) is placed. What happens: If ob1 is free, the call is granted; if ob2 comes and the monitor is not free, it must wait until ob1 finishes execution, then ob2 is given a chance. This is the synchronized block way to assure threads are properly synchronized and execution happens as wanted. Previous method used synchronized before the method definition; this method uses a synchronized block inside the caller. Both show the problems of synchronization and how to avoid them using synchronized methods and synchronized blocks. Result: same atomic bracket output as Solution 1, with finer control over which statements are protected.
When to choose which form: Use a synchronized method when the whole method touches shared state and you own the class. Use a synchronized(target){...} block when you only need to guard a call site or when you do not own the source of the shared class. The lock object matters: all threads must synchronize on the same object instance (target here) to cooperate. Different lock objects give no mutual exclusion.
23.8.6 Effect on Timing versus Correctness
Synchronization affects functioning because purposely keeping the monitor locked for 1000 milliseconds makes output come later but keeps correctness. The trade-off between timing and atomicity is explicit: unsynchronized runs faster in wall-clock interleaving but produces garbled brackets; synchronized runs slower but preserves unit order. That distinction is used to justify synchronization overhead.
Assumption — synchronization does not make code faster: Its purpose is correctness for shared memory, not speed. If shared data is not involved, extra synchronization can reduce throughput by forcing sequential entry. Synchronize the critical section only, and keep non-shared work outside the lock.
23.8.7 Practical Guidance for Practice
It is stressed that slides will be uploaded to the portal and students are highly recommended to take the slides and examples and practice on their own to get a good understanding at practice level. Changing sleep time and join handling and observing operating system response is advised. The lecture closed after covering threads, difference between process and thread, creation via Thread class or Runnable, different constructors, runnable interface, many examples, multi-threading creation, problems, sleep and join uses, priorities, and synchronization.
Recap — synchronization in one rule: Share memory, then share the lock — wrap the mutually exclusive block that touches the shared resource in a monitor so only one thread owns it at a time, using either a synchronized method or a synchronized(target) block. The CallMe bracket demo shows the unsynchronized interleaving that breaks atomicity and the two synchronized fixes that restore it at the cost of waiting. Exam note: be ready to predict bracket output with and without synchronized, and to explain the monitor as a token that only one thread can hold for a critical section.
Exam Guidance Summary
This lecture gave no separate mark distribution or count of exam questions and did not state what is excluded from the exam, nor any time-management hints for the paper. The exam-relevant guidance that was given is practical and centered on hands-on tracing.
What the lecturer said to practice: Take the slides once uploaded to the portal and run every thread example on your own machine. Vary sleep durations, vary join usage, change priority values, and observe how output order and timing shift. Execution on your own system shows that thread ids such as 14, 18, 17, 16, 15 and printed interleavings vary across runs and JVMs, so you learn to reason about scheduler behavior rather than memorizing one order.
Core topics to master for the exam: The difference between process and thread covering separate versus shared memory, negligible versus high corruption risk when unsynchronized, and expensive versus cheap context switching; the four Thread constructors including no-arg, name-only, Runnable-only, and Runnable plus name; the two data members name and priority with default priority 5 (NORM_PRIORITY), range 1 to 10, and methods getName, setName, getPriority, setPriority, isAlive, currentThread, activeCount; the Runnable interface requirement of exactly public void run() and the fact that Thread itself implements Runnable so both paths converge on run; the thread life-cycle states New, Runnable, Running, Blocked or Waiting, Timed Waiting, Terminated or Dead and the moves via start, sleep, join, and run return; the use of Thread.sleep for timed waiting and Thread.join together with isAlive for waiting until completion; and the monitor as a token for the mutually exclusive block with the two synchronized forms — synchronized method and synchronized(target) block — that make a critical section atomic even when it contains sleep(1000).
What exam tasks are likely: Writing or tracing thread-creation code using either extends Thread or implements Runnable, predicting the printed order of brackets or ids with and without synchronized, explaining why context switching between threads is cheaper than between processes, and explaining why unsynchronized access to shared structures such as the CallMe bracket printer or the BoundedQueue corrupts output. Prepare to show the shared target object and which code block is the critical section.
Exam note — how to study: Run the five-thread loops for both Thread and Runnable, then the child 1000 ms versus main 500 ms sleep demo, then the join demo, then the unsynchronized versus synchronized CallMe bracket demo. For each, note which threads share which object, which block is protected, and why sleep widens but does not create the race. Keep answers focused on sharing, timing, and monitor ownership. That hands-on experimentation builds the intuition the operating system responses require.
Key Industry Applications
Where threading matters beyond the classroom: The same shared-memory trade-off that lets three threads divide input, processing, and printing inside one process shapes desktop apps, servers, and background services. Correct threading keeps interfaces responsive, hardware busy, and shared resources safe.
Everyday process picture: the music player, web browser, and Word or Excel document as separate processes illustrate multitasking on a single CPU. The operating system context-switches the whole process state, so each keeps private memory. This model scales to any desktop where you run several programs at once.
Inside one program the lecture's input–processing–printing split is the common template for responsiveness. A text editor uses one thread to handle typing, a second to format text and check spelling, and a third to write to disk. A browser uses threads to load multiple images into a page while you scroll. Efficient use of the shared memory area by threads saves memory and reduces context-switch time, which matters when the system handles many short tasks.
Java's main thread anchors every application. When a program starts, the JVM creates the main thread automatically, and any worker threads you create with the Thread class or the Runnable interface run alongside it. The JVM together with the underlying operating system decides thread ids and scheduling, so ids such as 14, 15, 16, 17, 18 observed in the lecture are JVM-assigned and vary across machines and runs — a reality visible in real deployments where the same build behaves slightly differently on two machines.
Web servers, background jobs, and interactive tools rely on sleep for timed waiting and join for waiting for completion to coordinate workers. A server sleeps poll threads between checks and joins workers before shutting down. An animation loop sleeps between frames while the user-interface thread stays responsive.
Finally, monitors with synchronized methods and synchronized(target) blocks protect shared resources wherever interleaving would corrupt output — from the lecture's bracket printer to production queues, caches, loggers, and counters that many threads update. The textbook BoundedQueue example shows the same idea: queue add and remove must be guarded so two producers do not overwrite the same tail slot. The pattern is to identify the mutually exclusive block, pick one lock object that all threads agree on, and hold that lock only for the critical steps.
Takeaway for practice: Think of threads as roommates sharing one house — share the house to move fast, but lock the kitchen when one roommate is cooking a multi-step meal. Use separate processes when you need separate houses. That picture guides when to share memory, when to pay the switch cost, and when to use a monitor to keep correctness.
OODAP Lecture 23 notes · Multithreading in Java
Sections Breakdown
Defines process as program in execution with private memory illustrated by music player browser and file, explains context switching between P1 P2 P3 and multitasking illusion on single CPU.
Defines thread as independent programmed unit inside a process, shows multithreading one level down with T1 T2 sharing same address space and input-processing-printing example.
Contrasts process vs thread on memory sharing, corruption risk requiring careful synchronization, and context switch cost with table summary.
Explains thread states New Runnable Running Blocked Waiting Timed Waiting Terminated with timed sleep 1000 for T1 and 500 for T2 interleaving example.
Covers two ways to create threads via Thread class and Runnable interface, four Thread constructors, name priority fields default 5, key methods, and try catch need.
Five single-thread examples: extending Thread, plain Thread with defaults, Runnable via Thread, Runnable+name fourth constructor, and main thread inspection with setName setPriority showing Thread[main,5,main].
Shows five-thread loops via Thread and Runnable, sleep timed waiting 1000 vs 500 interleaving, join with isAlive waiting for completion, combined sleep+join and nondeterministic id order note.
Explains shared-resource corruption, monitor as token for mutually exclusive block, unsynchronized CallMe bracket demo with Hello Java Programming garbling, and two fixes via synchronized method and synchronized block with timing vs correctness tr...
Advises hands-on practice of all thread examples varying sleep and join, lists core examinable topics from process vs thread to monitor synchronization.
Maps lecture threading to browsers, editors, servers, animations and BoundedQueue showing shared-memory savings and monitor use in production.
Exam Revision Notes
Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.
Preliminaries — Process, Context Switching and Multitasking
Must-know: Process is program instance with private address space; OS gives illusion of parallelism by fast context switching; multitasking is perceived simultaneity
Top pitfall: Thinking processes share memory or run truly parallel on single CPU
Self-check: Why can P1 and P2 not corrupt each other memory while threads can?
Connects to: 23.2, 23.3
Thread and Multithreading Fundamentals
Must-know: Thread is part of a process scheduled independently; threads of one process share same address space and data structures
Top pitfall: Calling a thread a separate program or expecting separate memory
Self-check: Why do threads of P1 share memory while P1 and P2 do not?
Connects to: 23.1, 23.3
Process versus Thread — A Direct Comparison
Must-know: Processes separate memory no corruption high switch cost; threads share memory high corruption risk low switch cost due to same address space
Top pitfall: Assuming thread switch cost equals process switch cost or that threads are isolated
Self-check: Which is cheaper to switch and why?
Connects to: 23.2, 23.4
Thread Life Cycle and States
Must-know: New->Runnable via start, Runnable->Running via scheduler, Running->Timed Waiting via sleep, back to Runnable on wake, Running->Terminated when run returns
Top pitfall: Calling run() directly instead of start() so no new thread is created
Self-check: What state is a thread in after start but before scheduler picks it?
Connects to: 23.5, 23.7
Creating Threads in Java — Thread Class and Runnable Interface
Must-know: Four constructors: no-arg, String name, Runnable, Runnable+String; default priority 5; Runnable requires public void run; Thread implements Runnable
Top pitfall: Using wrong run signature or ignoring InterruptedException from sleep/join
Self-check: Which constructor takes both Runnable and name and what is default priority?
Connects to: 23.6, 23.5.moment.1
Worked Examples of Single Thread Creation
Must-know: a.start() enters run in new thread; new Thread() alone prints default name Thread-0; new Thread(runnable) and new Thread(runnable,name) map tasks via constructors; main thread is Thread[main,5,main]
Top pitfall: Forgetting to call start or expecting plain Thread to do work without overriding run
Self-check: What prints for Thread[main,5,main] and what changes after setName and setPriority(2)?
Connects to: 23.5, 23.7
Multithreading in Practice — Creating and Controlling Multiple Threads
Must-know: Loop start creates 5 threads with JVM-assigned ids 14 18 17 16 15 out of order; sleep 1000 for child and 500 for main gives ~2 main turns per child turn; join waits for thread death; sleep is static
Top pitfall: Assuming creation order equals execution order or that join protects shared data
Self-check: Why does child 1000 and main 500 cause two main prints per child print?
Connects to: 23.4, 23.8
Thread Synchronization — Monitors, Synchronized Methods and Blocks
Must-know: Monitor is object token only one thread holds; synchronized method locks implicit monitor even while sleeping; synchronized(target) block gives same atomic brackets [Hello] etc.
Top pitfall: Synchronizing on different objects so threads still interleave, or assuming sleep releases monitor
Self-check: How do synchronized method and synchronized block differ in making call mutually exclusive?
Connects to: 23.3, 23.7
Was this lecture useful?
BitsNotes AI Assistant
Subject Notes AssistantConfigure AI Chat
Choose how to access the chatbotSigned in as
Powered by BitsNotes — 20 messages per day. No API key needed. Want unlimited access? Use "Bring Your Own Key" mode.
Sign in to use AI Chat
Get 20 free AI messages per day to ask questions about your lecture notes. Sign in with Google or GitHub — it takes 5 seconds.
Sign In to BitsNotesSwitch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.