Today, I will introduce the use of the synchronized keyword to protect variables or methods during execution. When multiple threads need to access the value of a shared variable, adding the synchronized keyword before its usage ensures that only one thread can access this shared variable at a time. Other threads will be in a waiting state, waiting for the absence of any other thread executing on this shared variable before they get a chance to access it.
If we do not use synchronization or any other synchronization mechanism, multiple threads can simultaneously access and modify a shared variable, which can lead to data inconsistency, race conditions, and other errors. For example, let’s consider a scenario where we want to use two threads to add elements to the same List. Due to the lack of synchronization, both threads might read the size of the List as 0 simultaneously, and each of them will add an element, then write back the updated size.
Here is an example of the scenario without using synchronization:
import java.util.ArrayList;
import java.util.List;
public class NonSynchronizedExample {
private static List<Integer> sharedList = new ArrayList<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
sharedList.add(i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
sharedList.add(i);
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final size of the list: " + sharedList.size());
#Final size of the list: 1986
}
}
Sure, to add synchronization and ensure proper thread safety in the previous example, we can use the synchronized
keyword or other concurrency utilities, such as synchronized
blocks or the Collections.synchronizedList
method. Below is an example of using synchronized
blocks to achieve synchronization:
import java.util.ArrayList;
import java.util.List;
public class SynchronizedExample {
private static List<Integer> sharedList = new ArrayList<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
addToSharedList(i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
addToSharedList(i);
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final size of the list: " + sharedList.size());
#Final size of the list: 2000
}
private static void addToSharedList(int element) {
synchronized (sharedList) {
sharedList.add(element);
}
}
}
You can use the synchronized keyword directly on a method to synchronize the entire method, ensuring that only one thread can enter and execute the method at a time.
Below is an example of using synchronized on a method:
import java.util.ArrayList;
import java.util.List;
public class SynchronizedExample {
private static List<Integer> sharedList = new ArrayList<>();
// or List<String> list = Collections.synchronizedList(new ArrayList<String>());
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
addToSharedList(i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
addToSharedList(i);
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final size of the list: " + sharedList.size());
#Final size of the list: 2000
}
private synchronized static void addToSharedList(int element) {
// if Collections.synchronizedList ins implemented then dont need to use synchronized
sharedList.add(element);
}
}