If multiple threads are operating on a shared variable at the same time, and the operations are not atomic, the result would be unpredictable. In order to solve this problem, we could put synchronized blocks around the operations, to make sure only at most one thread could operate on the variable at one time.

Configuration
Java Compilation:
Java Runtime:
JDK 11.0.12
JRE HotSpot 11.0.12
If multiple threads is operating on a shared variable at the same time, and the operations are not atomic, the result would be unpredictable
In order to solve this problem, we could put synchronized blocks around the operations, to make sure only at most one thread could operate on the variable at one time
First let's see what will happen if synchronized is not used.
Create a main class with a static variable, with a static method for modifying the variable. Create 2 classes inheriting from Thread, 1 for adding to the variable and 1 for subtracting. Create 1000 instances of each and run all of them at the same time. You can see without the synchronized blocks, the result is not expected
import java.util.Random; public class MainClass { private static int bankAccount = 0; public static void updateBankAccount(int amount) { // Sleep for 0 to 1s to allow some time for the thread to "run" try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException ie) { ie.printStackTrace(); } bankAccount += amount; } public static void main(String[] args) throws Exception { int THREADS = 1000; System.out.println("At the start, bank has " + bankAccount); CustomerDepositThread[] customerDepositThreads = new CustomerDepositThread[THREADS]; CustomerWithdrawThread[] customerWithdrawThreads = new CustomerWithdrawThread[THREADS]; for (int i = 0; i < THREADS; i++) { customerDepositThreads[i] = new CustomerDepositThread(); customerWithdrawThreads[i] = new CustomerWithdrawThread(); customerDepositThreads[i].start(); customerWithdrawThreads[i].start(); } for (int i = 0; i < THREADS; i++) { customerDepositThreads[i].join(); customerWithdrawThreads[i].join(); } System.out.println("At the end, bank has " + bankAccount); } }
CustomerDepositThread.java:
public class CustomerDepositThread extends Thread { @Override public void run() { MainClass.updateBankAccount(10); } }
CustomerWithdrawThread.java:
public class CustomerWithdrawThread extends Thread { @Override public void run() { MainClass.updateBankAccount(-10); } }
At the end, bankAccount is not 0:
At the start, bank has 0
At the end, bank has -100
To resolve this issue, just put synchronized block around the updating statement. In this case, the statements inside the synchronized block will not be executed by more than 1 thread at the same time:
public static void updateBankAccount(int amount) { // Sleep for 0 to 1s to allow some time for the thread to "run" try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException ie) { ie.printStackTrace(); } synchronized (MainClass.class) { bankAccount += amount; } }
This time bankAccount is 0 at the end:
At the start, bank has 0
At the end, bank has 0
In the synchronized block, you have to put an object so that only synchronized blocks sharing the same object will be synchronized. In this example there is just one synchronized block so we just put the MainClass.class variable.
However, in some scenarios like there are 2 totally separate data to protect, using the same object on synchronized block does not make sense and it will also slow down the performance since there are less concurrency if only 1 thread can do things at one time. So it this case, we should create 2 different locking objects for the synchronized blocks.
See the below example which we have 2 different locking objects, which is to protect 2 different bank accounts. Supposed we have CustomerDepositThread2 and CustomerWithdrawThread2 which are calling updateBankAccount2 which are operating on bankAccount2
import java.util.Random; public class MainClass { private static int bankAccount = 0; private static int bankAccount2 = 0; private static Object lockObjectBankAccount = new Object(); private static Object lockObjectBankAccount2 = new Object(); public static void updateBankAccount(int amount) { // Sleep for 0 to 1s to allow some time for the thread to "run" try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException ie) { ie.printStackTrace(); } synchronized (lockObjectBankAccount) { bankAccount += amount; } } public static void updateBankAccount2(int amount) { // Sleep for 0 to 1s to allow some time for the thread to "run" try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException ie) { ie.printStackTrace(); } synchronized (lockObjectBankAccount2) { bankAccount2 += amount; } } public static void main(String[] args) throws Exception { int THREADS = 1000; System.out.println("At the start, bank has " + bankAccount); System.out.println("At the start, bank2 has " + bankAccount2); CustomerDepositThread[] customerDepositThreads = new CustomerDepositThread[THREADS]; CustomerWithdrawThread[] customerWithdrawThreads = new CustomerWithdrawThread[THREADS]; CustomerDepositThread2[] customerDepositThreads2 = new CustomerDepositThread2[THREADS]; CustomerWithdrawThread2[] customerWithdrawThreads2 = new CustomerWithdrawThread2[THREADS]; for (int i = 0; i < THREADS; i++) { customerDepositThreads[i] = new CustomerDepositThread(); customerWithdrawThreads[i] = new CustomerWithdrawThread(); customerDepositThreads2[i] = new CustomerDepositThread2(); customerWithdrawThreads2[i] = new CustomerWithdrawThread2(); customerDepositThreads[i].start(); customerWithdrawThreads[i].start(); customerDepositThreads2[i].start(); customerWithdrawThreads2[i].start(); } for (int i = 0; i < THREADS; i++) { customerDepositThreads[i].join(); customerWithdrawThreads[i].join(); customerDepositThreads2[i].join(); customerWithdrawThreads2[i].join(); } System.out.println("At the end, bank has " + bankAccount); System.out.println("At the end, bank2 has " + bankAccount2); } }
CustomerDepositThread2.java:
public class CustomerDepositThread2 extends Thread { @Override public void run() { MainClass.updateBankAccount2(10); } }
CustomerWithdrawThread2.java:
public class CustomerWithdrawThread2 extends Thread { @Override public void run() { MainClass.updateBankAccount2(-10); } }