Detailed explanation of Java concurrent programming built in lock of synchronized

  • 2021-09-05 00:02:07
  • OfStack

Brief introduction

synchronized is a heavyweight lock in the early version of JDK 5.0, which is very inefficient. However, since JDK 6.0, JDK has made a lot of optimizations on the keyword synchronized, such as bias lock and lightweight lock, which has greatly improved its efficiency.

The function of synchronized is to realize the synchronization between threads. When multiple threads need to access the shared code area, the shared code area is locked, so that only one thread can access the shared code area every time, thus ensuring the security between threads.

Because there is no explicit locking and unlocking process, it is called implicit lock, also called built-in lock and monitor lock.

In the following example, when multiple threads access a shared code region without synchronized, different results may occur than expected.


public class Apple implements Runnable {
 private int appleCount = 5;

 @Override
 public void run() {
  eatApple();
 }

 public void eatApple(){
  appleCount--;
  System.out.println(Thread.currentThread().getName() + " Eat 1 An apple, and there is still left " + appleCount + " An apple ");
 }

 public static void main(String[] args) {
  Apple apple = new Apple();
  Thread t1 = new Thread(apple, " Xiaoqiang ");
  Thread t2 = new Thread(apple, " Xiao Ming ");
  Thread t3 = new Thread(apple, " Floret ");
  Thread t4 = new Thread(apple, " Xiao Hong ");
  Thread t5 = new Thread(apple, " Xiao Hei ");
  t1.start();
  t2.start();
  t3.start();
  t4.start();
  t5.start();
 }
}

The following results may be output:

Xiaoqiang ate one apple and there were three apples left
Little Black ate one apple and there were three apples left
Xiao Ming ate one apple and there were two apples left
The little flower ate one apple, and there was one apple left
Xiaohong ate one apple, and there were 0 apples left

The reason for the abnormal output result is that the operation in eatApple method is not atomic. For example, when A thread completes the assignment of appleCount and has not yet output, B thread obtains the latest value of appleCount and completes the assignment operation, and then A and B are output at the same time. (A and B threads correspond to Xiaohei and Xiaoqiang respectively)

If the eatApple method is changed as follows, will there be thread safety problems?


public void eatApple(){
	System.out.println(Thread.currentThread().getName() + " Eat 1 An apple, and there is still left " + --appleCount + " An apple ");
}

There will still be, because--appleCount is not an atomic operation,--appleCount can be expressed in another way: appleCount = appleCount-1, and it is still possible to have the above abnormal output results.

Use of synchronized

synchronized is divided into two usages: synchronous method and synchronous code block. When each thread accesses the synchronous method or synchronous code block area, it needs to obtain the lock of the object first, and the thread that grabs the lock can continue to execute, while the thread that cannot grab the lock is blocked, waiting for the thread that grabs the lock to release the lock after the execution is completed.

1. Synchronize code blocks

The object of the lock is object:


public class Apple implements Runnable {
 private int appleCount = 5;
 private Object object = new Object();

 @Override
 public void run() {
  eatApple();
 }

 public void eatApple(){
	// Synchronize the code block, where the object of the lock is object
  synchronized (object) {
   appleCount--;
   System.out.println(Thread.currentThread().getName() + " Eat 1 An apple, and there is still left " + appleCount + " An apple ");
  }
 }

  //... Omission main Method 
}

2. Synchronize the method, modifying the common method

The object of the lock is the instance object of the current class:


public class Apple implements Runnable {
 private int appleCount = 5;

 @Override
 public void run() {
  eatApple();
 }

 public synchronized void eatApple() {
  appleCount--;
  System.out.println(Thread.currentThread().getName() + " Eat 1 An apple, and there is still left " + appleCount + " An apple ");
 }

 //... Omission main Method 
}

Equivalent to the following synchronization code block:


public void eatApple() {
	synchronized (this) {
		appleCount--;
		System.out.println(Thread.currentThread().getName() + " Eat 1 An apple, and there is still left " + appleCount + " An apple ");
	}
}

3. Synchronizing methods, decorating static methods

The locked object is the class object of the current class:


public class Apple implements Runnable {
 private static int appleCount = 5;

 @Override
 public void run() {
  eatApple();
 }

 public synchronized static void eatApple() {
  appleCount--;
  System.out.println(Thread.currentThread().getName() + " Eat 1 An apple, and there is still left " + appleCount + " An apple ");
 }

 //... Omission main Method 
}

Equivalent to the following synchronization code block:


public static void eatApple() {
	synchronized (Apple.class) {
		appleCount--;
		System.out.println(Thread.currentThread().getName() + " Eat 1 An apple, and there is still left " + appleCount + " An apple ");
	}
}

4. Difference between synchronization method and synchronization code block

a. The object locked by the synchronization method is the instance object of the current class or the class object of the current class, while the object locked by the synchronization code block can be any object.

b. The synchronization method uses the synchronized decorative method, while the synchronization code block uses the synchronized to decorate the shared code region. Synchronization code blocks have finer granularity and smaller lock areas than synchronization methods, and the smaller the lock range, the higher the efficiency. Synchronizing code blocks is obviously more applicable in the following situations:


public static void eatApple() {
	// Time-consuming operations that do not require synchronization 1
	//...
	synchronized (Apple.class) {
		appleCount--;
		System.out.println(Thread.currentThread().getName() + " Eat 1 An apple, and there is still left " + appleCount + " An apple ");
	}
	// Time-consuming operations that do not require synchronization 2
	//...
}

Reentrancy of built-in locks

The reentrancy of built-in locks means that when a thread tries to acquire a lock that it already holds, it always succeeds. As follows:


public static void eatApple() {
	synchronized (Apple.class) {
		synchronized (Apple.class) {
			synchronized (Apple.class) {
				appleCount--;
				System.out.println(Thread.currentThread().getName() + " Eat 1 An apple, and there is still left " + appleCount + " An apple ");
			}
		}
	}
}

If the lock is not reentrant, then if a thread holds the lock and then has to wait for the thread holding the lock to release the lock, wouldn't it cause a deadlock?

Can synchronized be inherited?

synchronized cannot be inherited, and the synchronized keyword needs to be added manually if the overridden method in the subclass needs to be synchronized.


public class AppleParent {
 public synchronized void eatApple(){

 }
}

public class Apple extends AppleParent implements Runnable {
 private int appleCount = 5;

 @Override
 public void run() {
  eatApple();
 }

 @Override
 public void eatApple() {
  appleCount--;
  System.out.println(Thread.currentThread().getName() + " Eat 1 An apple, and there is still left " + appleCount + " An apple ");
 }

 //... Omission main Method 
}

Wait and wake-up based on built-in locks

The built-in lock-based wait and wake-up is implemented using wait () and notify () or notifyAll () in the Object class. These methods are called only from synchronous methods or blocks of synchronous code because they already hold the corresponding lock. If called without acquiring the corresponding lock, an IllegalMonitorStateException exception will be thrown. Here are some related methods:

wait (): Causes the current thread to wait indefinitely until another thread calls notify () or notifyAll ().

wait (long timeout): Specifies a timeout after which the thread will wake up automatically. Threads can also be awakened by notify () or notifyAll () before the timeout. Note that wait (0) is equivalent to calling wait ().

wait (long timeout, int nanos): Similar to wait (long timeout), the main difference is that wait (long timeout, int nanos) provides higher accuracy.

notify (): Randomly wakes up a thread waiting on the same lock object.

notifyAll (): Wakes up all threads waiting on the same lock object.

A simple example of waiting to wake up:


public void eatApple(){
	System.out.println(Thread.currentThread().getName() + " Eat 1 An apple, and there is still left " + --appleCount + " An apple ");
}
0

public void eatApple(){
	System.out.println(Thread.currentThread().getName() + " Eat 1 An apple, and there is still left " + --appleCount + " An apple ");
}
1

public void eatApple(){
	System.out.println(Thread.currentThread().getName() + " Eat 1 An apple, and there is still left " + --appleCount + " An apple ");
}
2

Output:

Xiaoming bought five apples
Xiaohong ate an apple
Xiaohong ate an apple
Xiaohong ate an apple
Xiaohong ate an apple
Xiaohong ate an apple
Xiaoming bought five apples
Xiaohong ate an apple
......


Related articles: