English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Detailed Introduction and Example Code of Java Thread Synchronization

Java Thread Synchronization

Summary:

To speed up the execution of the code, we have adopted the multi-threading method. Parallel execution indeed makes the code more efficient, but the problem that comes with it is that there are many threads running simultaneously in the program. If they attempt to modify an object at the same time, it is likely to cause errors. In this case, we need to use a synchronization mechanism to manage these threads.

(I) Race Condition

In the operating system, there is a picture that I remember very well. It depicts several processes, with several threads inside these processes, all of which point uniformly to the resources of the process. It is the same in Java; resources are shared among threads rather than each thread having an independent set of resources. In this shared scenario, it is very likely that multiple threads will be accessing a resource simultaneously, a phenomenon we call a race condition.

In a banking system, each thread manages a separate account, and these threads may perform transfer operations.
When operating in a thread, it first stores the account balance in a register, then in the second step, it reduces the number in the register by the amount to be transferred, and in the third step, it writes the result back to the balance.
The problem is, this thread finishes executing1,2At this time, another thread is woken up and modifies the account balance value of the first thread, but at this moment, the first thread is not aware of this. After the second thread finishes its operation, the first thread continues with its third step: writing the result back to the balance. At this point, it wipes out the operation of the second thread, so the total amount of the entire system will definitely be wrong.
This is the adverse situation of Java's competition condition.

(II) ReentrantLock class

The above example tells us that if our operation is not an atomic operation, an interruption is definitely going to happen, even if the probability is really very small at times, but this situation cannot be ruled out. We cannot make our code become like atomic operations in the operating system, what we can do is to lock our code to ensure safety. In concurrent programs, if we want to access data, we first lock our code before we access it, and during the time we use the lock, the resources involved in our code are like being 'locked', and cannot be accessed by other threads until we unlock.

In Java, the synchronized keyword and the ReentrantLock class both have the function of a lock. Let's discuss the function of ReentrantLock first.

1.ReentrantLock constructor

In this class, there are two constructors provided, one is the default constructor, which is nothing to say about, and one is a constructor with a fairness policy. This fairness policy is much slower than the normal lock, and secondly, it is not truly fair in some cases. And if we have no special reason to really need the fairness policy, we should try not to study this policy.

2.Acquire and release

ReentrantLock myLock = new ReentrantLock();
//Create an object
myLock.lock();
//Acquire the lock
try{
...
}
finally{
myLock.unlock();
//Release the lock
}

Be sure to release the lock in the finally block! As we mentioned before, unchecked errors can cause thread termination. An unexpected termination will stop the program from running further, and if the release is not placed in the finally block, this lock will never be released. This is the same principle as using .close() after a package in our daily framework. Speaking of close, it is worth mentioning that when we use a lock, we cannot use 'resource-bound try statements' because this lock is not closed with close. If you don't know what resource-bound try statements are, then just consider this sentence as not said.

3The lock is reentrant

If you need to use a lock in recursive or loop programs, you can use it with confidence. The ReentrantLock lock is reentrant, it maintains a count of the number of times it is called each time lock() is called, and it must be released with unlock() after each lock call.

(三)条件对象

Generally, when a thread enters the critical section after locking and finds that the resources it needs are being used by other objects or do not meet the conditions for execution, at this time we need to use a condition object to manage these threads that have obtained a lock but cannot perform any useful work.

if(a>b){
  a.set(b-1);
}

1“自己困住了自己”

The above is a very simple condition judgment, but we cannot write like this in concurrent programs. The problem is that if another thread is woken up just after this thread finishes the judgment and the other thread operates to make a less than b (the condition of the if statement is no longer correct).

At this point, we may think that we can put the entire if statement directly inside the lock to ensure that our code is not interrupted. However, this also brings up a problem. If the if judgment is false, the statements inside the if will not be executed. But if we need to execute the statements inside the if, or even wait until the if judgment becomes correct before executing the statements inside the if, we suddenly find that the if statement will never become correct again because our lock has locked this thread, and other threads cannot access the critical section and modify the values of a and b to make the if judgment correct. This is really very embarrassing. Our own lock has trapped us, and we cannot get out, and others cannot come in.

2.Condition class

To solve this situation, we use the newCondition method in the ReentrantLock class to obtain a condition object.

Condition cd = myLock.newCondition();

After obtaining the Condition object, we should study what methods and functions this object has. Without rushing to look at the API, we return to the topic and find that the most urgent problem to be solved is the if condition judgment. How can we:When the lock is already acquired and an error in the if judgment is found, give other threads a chance and wait for the if judgment to become correct.

The Condition class was born to solve this problem. With the Condition class, we can directly follow the await method under the if statement, which indicates that this thread is blocked and has given up the lock, waiting for other threads to operate.

Note that the noun we use here is 'blocked'. We have also mentioned before that 'blocked' and 'waiting' are quite different: when waiting to acquire a lock, once the lock is idle, it can automatically acquire the lock. However, when blocking to acquire a lock, even if there is an available lock, it has to wait until the thread scheduler allows it to hold the lock.

Other threads, after successfully executing the content of the if statement, need to call the signalAll method, which will reactivate all threads that were blocked due to this condition, allowing them to regain the opportunity. These threads are allowed to access fromContinue at the point where it is blockedAt this point, the thread should retest the condition again, if it still does not meet the conditions, it needs to repeat the above operation again.

ReentrantLock myLock = new ReentrantLock();
//Create a lock object
myLock.lock();
//Lock the critical section below
Condition cd = myLock.newCondition();
//Create a Condition object, this cd object represents the condition object
while(!(a>b))
  cd.await();
//The above while loop and await method call is the standard writing style
//If the conditions cannot be met, then it will enter a blocking state, release the lock, and wait for others to activate it
a.set(b-1);
//Wait until you come out of the while loop and meet the judgment conditions, then execute your own function
cd.signalAll();
//Finally, you must not forget to call the signalAll method to activate other blocked threads
//If all threads are waiting for other threads to signalAll, then they enter a deadlock

It is very bad if all threads are waiting for other threads to signalAll, then they enter a deadlock state. Deadlock state refers to a situation where all threads need resources that are formed into a circular structure by other threads, causing none of them to be able to execute. It is necessary to call the signalAll method at the end to activate other 'brothers' who are blocked because of cd, which is convenient for everyone and can reduce the occurrence of deadlocks.

3.Summary of Condition objects and locks

In summary, there are several characteristics of Condition objects and locks.

  1. A lock can be used to protect code segments, and only one thread can enter the protected area at any given time
  2. A lock can manage threads that try to enter the critical section
  3. A lock can have one or more condition objects
  4. Each condition object manages those threads that cannot be executed due to the reasons described above but have already entered the protected code segment.

(4) synchronized keyword

The ReentrantLock and Condition objects we introduced above are a method to protect code segments. In Java, there is another mechanism: by using the synchronized keyword to modify methods, adding an internal lock to the method. Starting from a certain version, every object in Java has an internal lock, which protects those methods that are marked with synchronized. That is to say, if you want to call this method, you must first obtain the internal object lock.

1.synchronized vs ReentrantLock

Let's take out the above code:

public void function(){}
  ReentrantLock myLock = new ReentrantLock();
  myLock.lock();
  Condition cd = myLock.newCondition();
  while(!(a>b))
    cd.await();
  a.set(b-1);
  cd.signalAll();
}

If we use synchronized to implement this code, it will become the following:

public synchronized void function(){
  while(!(a>b))
    wait();
  a.set(b-1);
  notifyAll();
}

What we need to pay attention to is that when using the synchronized keyword, there is no need to use ReentrantLock and Condition objects anymore. We replace the await method with the wait method, and the notifyAll method with the signalAll method. This is indeed much simpler than before.

2.Synchronized static methods

It is also legal to declare a static method as synchronized. If this method is called, the internal lock of the related class object will be acquired. For example, when we call the static method of the Test class, the lock of the Test.class object will be locked.

3.Limitations of internal locks and conditions

Internal locks are simple, but they have many limitations:

  1. You cannot interrupt a thread that is trying to acquire a lock
  2. You cannot set a timeout when trying to acquire a lock
  3. Because it is not possible to instantiate a condition through Condition. Each lock has only one condition, which may not be enough

Which lock should be used in the code? Lock and Condition objects or synchronized methods? Some suggestions are given in the book Core Java:

  1. It is best not to use ReentrantLock or the synchronized keyword. In many cases, you can use the java.util.concurrent package
  2. If synchronized meets your code needs, please use it first
  3. Until if there is a special need for ReentrantLock, then use it

Thank you for reading, I hope it can help everyone, thank you for your support to our website!

You May Also Like