Lesson 9 | Synchronization |
Objective | Describe how and when to use synchronization in threads. |
Java Thread Synchronization
When you synchronize methods, you are indicating that only one thread can use that method at a time. If another thread wants to use a synchronized method currently in use, that thread must wait. Usually this works fine, but there is one scenario in which this can be disastrous and that is deadlock.
Deadlock:
Imagine that there are two threads, A and B. Thread A is waiting to access a synchronized method in thread B.
However, this method is in use by thread B, so thread A must wait. Thread B, in turn, is waiting to access a synchronized method in thread A, and this method in thread A is also in use. Both thread A and thread B are waiting on each other. This is what is meant by a deadlock.
Java Concurrency
Using join() method with Java Threads
In Java 1.1, the `join()` method in the Thread class is used to ensure that one thread completes its execution before another continues. This method allows a thread to wait for another thread to finish before proceeding, which is useful for synchronization.
How `join()` Works in Java 1.1 Threading Model
- Introduced in Java 1.0 and continued in Java 1.1, the
join()
method allows one thread to wait for another thread's completion.
- The thread that calls
join()
pauses execution until the target thread terminates.
- It is part of cooperative threading, where the developer manually controls execution flow rather than relying on preemptive scheduling.
Usage Example in Java 1.1
class MyThread extends Thread {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
try {
Thread.sleep(500); // Simulate work
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
}
}
}
}
public class JoinExample {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
try {
t1.join(); // Main thread waits for t1 to finish before starting t2
} catch (InterruptedException e) {
System.out.println("Main thread interrupted");
}
t2.start(); // Starts only after t1 completes
try {
t2.join(); // Ensures main thread waits for t2 to complete before exiting
} catch (InterruptedException e) {
System.out.println("Main thread interrupted");
}
System.out.println("Main thread exiting");
}
}
Behavior in Java 1.1 Threading Model
-
Sequential Execution Control:
- The main thread starts
t1
and waits for it to finish using t1.join()
.
- Only after
t1
completes does t2
start.
-
Blocking Mechanism:
- The calling thread (main thread) blocks until the target thread (
t1
or t2
) completes.
-
Thread Scheduling:
- Java 1.1 did not have thread priorities enforced by the JVM, so developers relied on
join()
for proper execution sequencing.
-
Cooperative Multitasking:
- In Java 1.1, thread scheduling was less preemptive, meaning threads relied on methods like
yield()
, sleep()
, and join()
to manage execution order.
join(timeout) in Java 1.1:
- A variant of
join()
allows a timeout value.
- This ensures that if the thread does not complete within the specified time, execution continues.
t1.join(1000); // Wait for t1 for 1 second before continuing
Key Takeaways
- Thread Synchronization: Ensures ordered execution.
- Blocking Behavior: Calling thread waits for the target thread to finish.
- Cooperative Threading: Before Java 1.2, JVM did not enforce strict thread priorities, so
join()
helped control execution order.
- Alternatives: Before Java 5 (which introduced Executors and CountDownLatch),
join()
was a common way to handle thread dependencies.
Thread Question discussing the join() method
What can be done so that the following program prints "tom", "dick" and "harry" in that order?
public class TestClass extends Thread{
String name = "";
public TestClass(String str) {
name = str;
}
public void run() {
try{
Thread.sleep( (int) (Math.random()*1000) );
System.out.println(name);
}
catch(Exception e){
}
}
public static void main(String[] str) throws Exception{
//1
Thread t1 = new TestClass("tom");
//2
Thread t2 = new TestClass("dick");
//3
t1.start();
//4
t2.start();
//5
System.out.println("harry");
//6
}
}
Select 1 option:
- Insert t1.join(); and t2.join(); at //4 and //5 respectively.
- Insert t2.join() at //5
- Insert t1.join(); t2.join(); at //6
- Insert t1.join(); t2.join(); at //3
- Insert t1.join() at //5
Answer: a
Explanation:
Here, you have 3 threads in action. The main thread and the 2 threads that are created in main.
The concept is when a thread calls
join()
on another thread, the calling thread waits till the other thread dies.
If we insert
t1.join()
at //4, the main thread will not call
t2.start()
till t1 dies. So, in affect, "tom" will be printed first and then t2 is started. Now, if we put
t2.join()
at //5,
the main thread will not print "harry" till t2 dies. So, "dick" is printed and then the main thread prints "harry".
