Multiple Locks Using Synchronization
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
- Improved Concurrency: Threads holding different locks can work independently without blocking each other.
- 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
: ARandom
instance to generate random integers.list1
andlist2
: TwoArrayList
instances used to store random integers. These lists are shared between all threads because they arestatic
.
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 onlist2
.
4. process()
- Purpose: Runs both
stageOne()
andstageTwo()
tasks 1,000 times, simulating a workload.
5. main()
Method
- Two threads (
t1
andt2
) are created, both running theprocess()
method, which means both threads executestageOne()
andstageTwo()
1,000 times each.
6. Joining Threads
- Purpose: Ensures the
main
thread waits for both threads (t1
andt2
) to complete before continuing. Withoutjoin()
, 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
andlist2
. 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:
- Shared Resource Access: Both threads modify
list1
andlist2
without any synchronization, leading to inconsistent sizes of the lists. - Race Condition: Multiple threads accessing
list1.add()
orlist2.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 accessstageTwo()
(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()
andstageTwo()
. - Synchronization forces them to execute one after another instead of concurrently accessing the shared resources (
list1
andlist2
).
- Both threads in the given code frequently call
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
orlist2
).
Multiple Locks:
- Use separate locks for different resources to allow more concurrent execution.
- Example: Use two
Object
locks forlist1
andlist2
.
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());
}
}