Learn Java multithreading synchronization

  • 2020-05-05 11:17:15
  • OfStack

If your java foundation is weak, or you don't know much about java multithreading, please read this article "learn about Java multithreading's thread definition, state, and properties"

Synchronization has always been a difficult part of java's multithreading, and we rarely used it when we were doing android development, but that's not the reason we're not familiar with synchronization. Hopefully this article will help more people understand and apply java synchronization.
In multithreaded applications, two or more threads share access to the same data. This is often called a race condition if two threads access the same object and each thread calls methods that modify the object.
The easiest example of competition condition to understand is: for example, the train ticket is sold, the train ticket is certain, but the train ticket window is everywhere, each window is equivalent to a thread, so many threads share all the train ticket resources. Moreover, there is no guarantee of its atomicity. If at a certain point in time, two threads use this resource at the same time, they will take out the same train ticket (the same seat number), which will cause trouble for passengers. The solution is that when a thread wants to use the train ticket resource, we give it a lock and wait for it to finish the work before giving the lock to another thread that wants to use the resource. That would not happen.

1. Lock object
The
synchronized keyword automatically provides locks and associated conditions, and synchronized is handy in most cases where explicit locks are required, but when we look at the ReentrantLock classes and condition objects, we can better understand the synchronized keyword. ReentrantLock was introduced with JAVA SE 5.0, and the structure of the code block protected with ReentrantLock is as follows:


mLock.lock();
try{
...
}
finally{
mLock.unlock();
}

This structure ensures that only one thread enters the critical section at any given time, and that once a thread blocks the lock object, no other thread can pass the lock statement. When other threads call lock, they are blocked until the first thread releases the lock object. It is necessary to place the unlock operation in finally, and if an exception occurs in the critical section, the lock must be released, otherwise other threads will block forever.

2. Condition object
When entering the critical region, only to find that a certain condition is satisfied, it can not be executed. To use a condition object to manage threads that have acquired a lock but cannot do useful work, the condition object is also called a condition variable.
Let's look at the following example to see why we need the conditional object

In a scenario where we need to transfer money by bank, we first write the bank's class, whose constructor needs to pass in the number of accounts and the amount of accounts


public class Bank {
private double[] accounts;
  private Lock bankLock;
  public Bank(int n,double initialBalance){
    accounts=new double[n];
    bankLock=new ReentrantLock();
    for (int i=0;i<accounts.length;i++){
      accounts[i]=initialBalance;
    }
  }
  }

Next we are going to draw, write a withdrawal method, from is transfer, to is receiver, amount transfer amount, the results we found that transfer balance is insufficient, if there are other threads for the transfer side to save enough money can transfer successful, but this thread has access to the lock, it is exclusive, other threads are unable to get the lock to deposit operation, this is the reason why we need to introduce condition object.


  public void transfer(int from,int to,int amount){
    bankLock.lock();
    try{
      while (accounts[from]<amount){
        //wait
      }
    }finally {
      bankLock.unlock();
    }
  }

A lock object has a number of related condition objects, you can use the newCondition method to get a condition object, we get the condition object after calling the await method, the current thread is blocked and give up the lock


public class Bank {
private double[] accounts;
  private Lock bankLock;
  private Condition condition;
  public Bank(int n,double initialBalance){
    accounts=new double[n];
    bankLock=new ReentrantLock();
    // Get the conditional object 
    condition=bankLock.newCondition();
    for (int i=0;i<accounts.length;i++){
      accounts[i]=initialBalance;
    }
  }
  public void transfer(int from,int to,int amount) throws InterruptedException {
    bankLock.lock();
    try{
      while (accounts[from]<amount){
        // Blocks the current thread and abandons the lock 
        condition.await();
      }
    }finally {
      bankLock.unlock();
    }
  }
}

The thread waiting for the lock is essentially different from the thread calling the await method, and once a thread calls the await method, it enters the wait set for that condition. When the lock is available, the thread cannot unlock immediately, instead it is blocked until another thread invokes the signalAll method on the same condition. When another thread is ready to transfer to our previous transferor, just call condition.signalAll (); This call reactivates all threads that were waiting because of this condition.
When a thread calls the await way he couldn't reactivate itself, in the hope that other threads to invoke signalAll method to activate itself, if there are no other threads that are waiting thread to activate, then can produce deadlock phenomenon, if all the other threads are blocked, the last active threads in the call prior to the termination of the other threads blocked state await, then it was blocked, there is no any thread can remove other threads blocked, program had been put up.
So when is signalAll called? Normally it would be nice to call signalAll while waiting for the thread's direction to change. In this case, when an account balance changes, the waiting thread should have a chance to check the balance.


 public void transfer(int from,int to,int amount) throws InterruptedException {
    bankLock.lock();
    try{
      while (accounts[from]<amount){
        // Blocks the current thread and abandons the lock 
        condition.await();
      }
      // Operation of transfer 
      ...
      condition.signalAll();
    }finally {
      bankLock.unlock();
    }
  }

Instead of immediately activating a waiting thread when the signalAll method is called, it simply unblocks the waiting thread so that it can compete for access to the object after the current thread exits the synchronized method. Another method is signal, which randomly unblocks a thread. If the thread still fails to run, it is blocked again. If no other thread calls signal again, the system is deadlocked.

3. Synchronized keyword
The Lock and Condition interfaces provide programmers with a high degree of locking control, but in most cases that control is not required and a mechanism embedded within the java language can be used. Starting with Java 1.0, each object in Java has an internal lock. If a method is declared with the synchronized keyword, the object's lock will protect the entire method. That is, to invoke the method, the thread must acquire an internal object lock.
In other words,


public synchronized void method(){

}

Equivalent to


public void method(){
this.lock.lock();
try{

}finally{
this.lock.unlock();
}

In the bank example above, we can declare the transfer method of the Bank class as synchronized instead of using a displayed lock.
There is only one condition associated with the internal object lock, wait zooming in to add a thread to the wait set, notifyAll or notify methods unblocking the waiting thread. So wait is equivalent to calling condition.await (), and notifyAll is equivalent to condition.signalAll ();

The transfer method in our example above can also be written as


  public synchronized void transfer(int from,int to,int amount)throws InterruptedException{
    while (accounts[from]<amount) {
      wait();
    }
    // Operation of transfer 
    ...
    notifyAll();  
    }

As you can see, writing code using the synchronized keyword is much more concise, and to understand this code, of course, you must understand that each object has an internal lock and that the lock has an internal condition. Threads that attempt to enter the synchronized method are managed by locks, and those that call wait are managed by conditions.

4. Synchronous blocking
As mentioned above, each Java object has a lock, and the thread can call the synchronization method to get the lock, and there is another mechanism to get the lock by entering a synchronization block, when the thread enters the following form of blocking:


synchronized(obj){

}

So he got the lock on obj. Let's look at Bank class


public class Bank {
private double[] accounts;
private Object lock=new Object();
  public Bank(int n,double initialBalance){
    accounts=new double[n];
    for (int i=0;i<accounts.length;i++){
      accounts[i]=initialBalance;
    }
  }
  public void transfer(int from,int to,int amount){
    synchronized(lock){
     // Operation of transfer 
      ...
    }
  }
}

Here, the lock object is created only to use the locks held by each Java object. Sometimes developers use a lock on an object to perform additional atomic operations, called client-side locking. For example, the Vector class, whose methods are synchronized. Now suppose that the bank balance

is stored in Vector

public class Bank {
private double[] accounts;
  private Lock bankLock;
  public Bank(int n,double initialBalance){
    accounts=new double[n];
    bankLock=new ReentrantLock();
    for (int i=0;i<accounts.length;i++){
      accounts[i]=initialBalance;
    }
  }
  }

0

The get and set methods of the Vecror class are synchronized, but this does not help us. After the first call to get, it is entirely possible for one thread to be stripped of its run in the transfer method, so that another thread might store a different value in the same storage location, but we can intercept the lock,


public class Bank {
private double[] accounts;
  private Lock bankLock;
  public Bank(int n,double initialBalance){
    accounts=new double[n];
    bankLock=new ReentrantLock();
    for (int i=0;i<accounts.length;i++){
      accounts[i]=initialBalance;
    }
  }
  }

1

Client-side locking (synchronized code blocks) is very fragile and is generally not recommended, and synchronization is generally best implemented with classes provided under the java.util.concurrent package, such as blocking queues. If the synchronization method is suitable for your program, use the synchronization method as much as possible, it can reduce the amount of code written, reduce the chance of error, if you need to use Lock/Condition structure to provide unique features, only use Lock/Condition.

The above is the entire content of this article, I hope to help you with your study.


Related articles: