Multithreading

Multiple Locks Using Synchronization

Spread the love

Multiple locks using synchronized blocks in Java is a concept where you create synchronization around different parts of the code by locking on different objects, instead of synchronizing the whole method or relying on a single lock. This allows multiple threads to access separate synchronized blocks simultaneously if the locks they are accessing are different, thereby improving concurrency and performance.

the synchronized block allows you to define a critical section and specify which object’s lock should be used to control access. Multiple locks can be achieved by creating separate objects and synchronizing blocks on these different objects.

Purpose of Using Multiple Locks

  1. Improved Concurrency: Threads holding different locks can work independently without blocking each other.
  2. Efficiency: Only the critical sections that need protection are locked, minimizing contention and improving performance.

Example without Synchronization

package com.rk.digital.school.synchronization;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Worker {
	private static Random random=new Random();
	private static List<Integer> list1=new ArrayList<Integer>();
	private static List<Integer> list2=new ArrayList<Integer>();
	
	public static void stageOne() {
		try {
			Thread.sleep(1);
		}catch (InterruptedException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		list1.add(random.nextInt(100));
	}
	
	public static void stageTwo() {
		try {
			Thread.sleep(1);
		}catch (InterruptedException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		list2.add(random.nextInt(100));
	}
	
	public static  void process() {
		for (int i = 0; i < 1000; i++) {
			stageOne();
			stageTwo();
		}
	}
	
	public static void main(String[] args) {
		System.out.println("Starting .........");
		
		long start=System.currentTimeMillis();
		
		Thread t1=new Thread(new Runnable() {
			@Override
			public void run() {
				process();
			}
		});
		
		Thread t2=new Thread(new Runnable() {
			@Override
			public void run() {
				process();
			}
		});
		
		t1.start();
		t2.start();
		try {
			t1.join();
			t2.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		long end =System.currentTimeMillis();
		
		System.out.println("Time taken:"+(end-start));
		
		System.out.println("List one is ===>"+list1.size());
		System.out.println("List Two is ===>"+list2.size());
	}
}

Output

Starting .........
Time taken:3119
List one is ===>1979
List Two is ===>1976

Explanation of Above code

1. Variables and Data Structures

  • random: A Random instance to generate random integers.
  • list1 and list2: Two ArrayList instances used to store random integers. These lists are shared between all threads because they are static.

2. stageOne()

  • Purpose: Simulates a task (stage one) where a random number is added to list1 after a brief pause (Thread.sleep(1)).

3. stageTwo()

  • Purpose: Similar to stageOne(), but operates on list2.

4. process()

  • Purpose: Runs both stageOne() and stageTwo() tasks 1,000 times, simulating a workload.

5. main() Method

  • Two threads (t1 and t2) are created, both running the process() method, which means both threads execute stageOne() and stageTwo() 1,000 times each.

6. Joining Threads

  • Purpose: Ensures the main thread waits for both threads (t1 and t2) to complete before continuing. Without join(), the program might print results before threads finish execution.

7. Measuring Execution Time

  • Measures and prints the time taken for the two threads to finish their tasks.

8. Printing the Results

  • Prints the size of list1 and list2. Each list should ideally have 2,000 elements (1,000 from each thread).
Potential Issues

This code does not use synchronization, which can result in race conditions:

  1. Shared Resource Access: Both threads modify list1 and list2 without any synchronization, leading to inconsistent sizes of the lists.
  2. Race Condition: Multiple threads accessing list1.add() or list2.add() concurrently could overwrite each other’s updates or cause data corruption.
Example with Synchronized method
package com.rk.digital.school.synchronization;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Worker {
	private static Random random=new Random();
	private static List<Integer> list1=new ArrayList<Integer>();
	private static List<Integer> list2=new ArrayList<Integer>();
	
	public static synchronized  void stageOne() {
		try {
			Thread.sleep(1);
		}catch (InterruptedException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		list1.add(random.nextInt(100));
	}
	
	public static synchronized void stageTwo() {
		try {
			Thread.sleep(1);
		}catch (InterruptedException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		list2.add(random.nextInt(100));
	}
	
	public static  void process() {
		for (int i = 0; i < 1000; i++) {
			stageOne();
			stageTwo();
		}
	}
	
	public static void main(String[] args) {
		System.out.println("Starting .........");
		
		long start=System.currentTimeMillis();
		
		Thread t1=new Thread(new Runnable() {
			@Override
			public void run() {
				process();
			}
		});
		
		Thread t2=new Thread(new Runnable() {
			@Override
			public void run() {
				process();
			}
		});
		
		t1.start();
		t2.start();
		try {
			t1.join();
			t2.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		long end =System.currentTimeMillis();
		
		System.out.println("Time taken:"+(end-start));
		
		System.out.println("List one is ===>"+list1.size());
		System.out.println("List Two is ===>"+list2.size());
	}
}

output

Starting .........
Time taken:6274
List one is ===>2000
List Two is ===>2000

if you see the above code out put its taking more time .

Explanation

When you use synchronized methods in Java, it tends to take more time because of the following reasons related to how synchronization impacts thread behavior and resource management:

1. Thread Blocking

  • When a thread executes a synchronized method, it acquires the lock for the object or class (depending on whether the method is instance-level or static-level).
  • If another thread attempts to access the same synchronized method, it must wait until the lock is released, leading to thread blocking and increased execution time.
  • Example: While one thread is running stageOne(), another thread trying to access stageTwo() (if both are synchronized methods) must wait for the lock to be released.

2. Limited Concurrency

  • Synchronization reduces the potential for concurrent execution.
  • Instead of multiple threads working independently on different parts of the program, they must coordinate access to synchronized methods, which creates a bottleneck.
  • For instance:
    • Both threads in the given code frequently call stageOne() and stageTwo().
    • Synchronization forces them to execute one after another instead of concurrently accessing the shared resources (list1 and list2).
Alternatives to Reduce Execution Time

Synchronized Blocks:

  • Instead of synchronizing the entire method, use synchronized blocks only around critical sections (e.g., adding to list1 or list2).

Multiple Locks:

  • Use separate locks for different resources to allow more concurrent execution.
  • Example: Use two Object locks for list1 and list2.

Example with synchronized block

package com.rk.digital.school.synchronization;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Worker {
	private static Random random=new Random();
	
	private static List<Integer> list1=new ArrayList<Integer>();
	private static List<Integer> list2=new ArrayList<Integer>();
	
	public static void stageOne() {
		try {
			Thread.sleep(1);
		}catch (InterruptedException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		synchronized (list1) {
		list1.add(random.nextInt(100));
		}
	}
	
	public static void stageTwo() {
		try {
			Thread.sleep(1);
		}catch (InterruptedException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		synchronized (list1) {
		list2.add(random.nextInt(100));
		}
	}
	
	public static  void process() {
		for (int i = 0; i < 1000; i++) {
			stageOne();
			stageTwo();
		}
	}
	
	public static void main(String[] args) {
		System.out.println("Starting .........");
		
		long start=System.currentTimeMillis();
		
		Thread t1=new Thread(new Runnable() {
			@Override
			public void run() {
				process();
			}
		});
		
		Thread t2=new Thread(new Runnable() {
			@Override
			public void run() {
				process();
			}
		});
		
		t1.start();
		t2.start();
		try {
			t1.join();
			t2.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		long end =System.currentTimeMillis();
		
		System.out.println("Time taken:"+(end-start));
		
		System.out.println("List one is ===>"+list1.size());
		System.out.println("List Two is ===>"+list2.size());
	}
}

Leave a Reply

Your email address will not be published. Required fields are marked *