Ten minutes take you to understand multithreading - multithreaded teamwork: synchronous control

Multithreaded teamwork: synchronous control

Synchronization control is an essential means for concurrent programs. The previously introduced keyword synchronized is one of the simplest control methods, which determines whether a thread can access critical zone resources. At the same time, object Wait() method and object The notify () method plays the role of thread waiting and notification. These tools play an important role in realizing complex multi-threaded cooperation. Next, we will first introduce the keywords synchronized and object Wait() method and object An alternative (or enhanced version) to the notify () method -- reentry locks.

1, Keyword synchronized function extension: Reentry lock

  1. Reentry locks can completely replace the keyword synchronized. In JDK
    In the early version of 5.0, the performance of reentry lock is much better than that of keyword synchronized. However, since JDK 6.0, JDK has made a lot of optimization on keyword synchronized, so that the performance gap between the two is not large.
  2. The reentry lock uses Java util. concurrent. locks. Reentrantlock class. The following is the simplest use case of reentry lock.
public class ReenterLock implements Runnable{
public static ReentrantLock lock=new ReentrantLock();
public static int i=0;
@override
public void run(){
for(int j=0;j<10000000;j++){
lock.lock();
try{
i++;}finally{
lock.unlock();
  }
 }
}
public static void main (String[] args) throws 
InterruptedException {
ReenterLock tl=new ReenterLock();
Thread t1=new Thread (tl);
Thread t2=new Thread(tl);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}}

  1. Lines 7 to 12 of the above code use the reentry lock to protect the critical zone resource i to ensure the safety of multithreading operation on i. As you can see from this code, compared with the keyword synchronized, the reentry lock has a displayed operation process. Developers must manually specify when to lock and when to release the lock. Because of this, the logic control flexibility of reentry lock is much better than that of keyword synchronized. However, it is worth noting that when exiting the critical area, you must remember to release the lock (line 11 of the code), otherwise other threads will have no chance to access the critical area again.
  2. You may be surprised by the name of the reentry lock. Why add the word "reentry" to the lock? In terms of class naming, Re-
    Reentrant lock is very appropriate. It is so called because this kind of lock can be accessed repeatedly. Of course, the iteration here is limited to one thread. Lines 7 to 12 of the above code can be written in the following form:
lock.lock();lock.lock();try{
i++;}finally{
lock. unlock();lock.unlock();
}
  1. In this case, it is allowed for a thread to obtain the same lock twice in a row. If this operation is not allowed, the same thread will deadlock with itself the second time it obtains a lock. The program will be "stuck" in the process of applying for lock for the second time. However, it should be noted that if the same thread obtains the lock multiple times, it must release the lock the same time. If you release the lock more times, you will get a Java Lang. illegalmonitorstateexception exception. On the contrary, if the number of times the lock is released is less, it is equivalent to that the thread still holds the lock. Therefore, other threads cannot enter the critical area.
  2. In addition to the flexibility of use, reentry lock also provides some advanced functions. For example, reentry locks can provide the ability to handle interrupts.

1, Interrupt response

  1. For the keyword synchronized, if a thread is waiting for a lock, there are only two results. Either it obtains the lock and continues to execute, or it keeps waiting. Using reentry locks provides another possibility that threads can be interrupted. That is, while waiting for the lock, the program can cancel the request for the lock as needed. Sometimes, this is very necessary. For example, if you make an appointment to play ball with your friend, if you wait for half an hour and your friend hasn't arrived, you suddenly receive a phone call saying that your friend can't come as scheduled due to an emergency, then you must go home disappointed. Interrupt provides a similar mechanism. If a thread is waiting for a lock, it can still receive a notification that it can stop working without waiting. This situation is helpful for dealing with deadlocks.
  2. The following code creates a deadlock, but thanks to the lock interrupt, we can easily solve the deadlock.
public class IntLock implements Runnable{
public static ReentrantLock lockl = new ReentrantLock();public static ReentrantLock lock2 = new ReentrantLock();int lock;
/**
★Control the locking sequence to facilitate the construction of deadlock * param lock
*/
public IntLock(int lock){
this.lock = lock;
}
@override
public void run(){
try {
if (lock == 1){
lock1. lockInterruptibly();try{
Thread.sleep(500);
}catch (InterruptedException e){1lock2. lockInterruptiblyO);
}else{
lock2 . lockInterruptibly();try{
Thread.sleep (500);
}catch (InterruptedException e){}lock1. lockInterruptiblyO);
] catch (InterruptedException e){
e.printStackTrace(;
]finally{
if(lock1.isHeldByCurrentThread ())
lockl.unlock ();
if (lock2.isHeldByCurrentThread()
lock2.unlock(;
System.out.println (Thread.currentThread ().getId()+":Thread exit");
public static void main(String[] args) throws InterruptedException{
IntLock r1 = new IntLock (1);
IntLock r2= new IntLock(2);Thread tl = new Thread (r1) ;Thread t2 = new Thread(r2);t1.start();t2.start();
Thread.sleep(1000);//Interrupt one of the threads
t2.interrupt ();
}}

  1. Thread t1 and
    After t2 is started, t1 occupies lock1 first and then lock2;t2 first occupies lock2 and then requests lock1 Therefore, it is easy to form mutual waiting between t1 and t2. Here, the lock interrupt () method is used uniformly for lock requests. This is a lock application action that can respond to interrupts, that is, it can respond to interrupts while waiting for locks.
  2. In line 47 of the code, the main thread is main
    In the sleep state, at this time, the two threads are in a deadlock state. In line 49 of the code, since the t2 thread is interrupted, t2 will abandon the application for lock1 and release the obtained lock2. This operation causes the t1 thread to get lock2 smoothly and continue to execute.
  3. Executing the above code will output:
java.lang. InterruptedException
at java.util.concurrent. locks.AbstractQueuedSynchronizer.doAcquireInterruptibly (AbstractQueuedSynchronizer.java:898)
at java.util.concurrent. locks. AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent. locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at geym.conc.ch3.synctrl.IntLock. run (IntLock.java: 31)at java.lang. Thread.run(Thread.java:745)
9:Thread exit
8:Thread exit

  1. It can be seen that after the interruption, both threads exit, but the only thing that really completes the work is tl, while t2 thread gives up its task and exits directly to release resources.

2, Time limit for lock application

  1. In addition to waiting for external notification, there is another way to avoid deadlock, that is, limited time waiting. Still take an appointment with a friend as an example. If a friend doesn't come and can't contact him, I think most people will be disappointed and leave after waiting for one to two hours. The same is true for threads. Usually, we can't judge why a thread can't get the lock for a long time. Maybe it's because of deadlock, maybe it's because of hunger. Given a waiting time, it makes sense for the system to let the thread give up automatically. We can use the tryLock() method to wait for a limited time.
  2. The following code shows the use of time limited wait lock.
public class TimeLock implements Runnable{
public static ReentrantLock lock=new ReentrantLock();@Override
public void run(){
try{
if(lock.tryLock (5,TimeUnit.SECONDS))
Thread.sleep(6000);
}else{
System.out.println ("get lock failed");
}catch (InterruptedException e){
e.printstackTrace();
}finally{if (lock.isHeldByCurrentThread0) lock.unlock();}
public static void main(String[] args){
TimeLock tl=new TimeLock();
Thread tl=new Thread(tl);Thread t2=new Thread(tl);t1.start();
t2.start();
}}
  1. Here, the tryLock() method receives two parameters, one representing the waiting time and the other representing the timing unit. The unit here is set to seconds and the duration is 5, which means that the thread can wait up to 5 seconds in this lock request. If the lock is not obtained for more than 5 seconds, false will be returned. Returns true if the lock is successfully obtained.
  2. In this example, because the thread occupying the lock will hold the lock for 6 seconds, another thread cannot obtain the lock within the waiting time of 5 seconds, so the lock request will fail.
  3. ReentrantLock. The trylock () method can also be run directly without parameters. In this case, the current thread will try to obtain the lock. If the lock is not occupied by other threads, the lock application will succeed and return true immediately. If the lock is occupied by another thread, the current thread will not wait, but will immediately return false. This mode does not cause threads to wait, so it does not cause deadlocks. This usage is demonstrated below:
public class TryLock implements Runnable{
public static ReentrantLock lock1 - new ReentrantLock (;public static ReentrantLock lock2 = new ReentrantLock(;int lock;
public TeyLock (int lock){
this.lock = lock;
@override
public void run ( {
if(1ock -- 1){
while (true) [
if(lockl.tryLock0{
tryf
try {
Thread.sleep (500);
}catch (InterruptedException e)[)
if (lock2.tryLock()){
try{
System.out.println/Thread.currentThread()
.getId( +":Ny Job done"];
return;
}finally{
lock2.unlock() ;
}}}
finally {
lock1.unlock();
}
} else{
while (true) {
if(lock2.tryLock()){
try {
try {
Thread.sleep(500);
] catch (InterruptedException e){
if( lock1.tryLock(){
try {
System.out.println (Thread.currentThread()
. getId()+":My Job done");
return;
} finally {
lock1.unlock();
}
]finally-{
lock2.unlock();
}
}
}
}
}

public static void main(String[] args) throws InterruptedException {
TryLock r1 = new TryLock(1);
TryLock r2= new Trylock(2);Thread tl = new Thread(r1);Thread t2 = new Thread(r2);t1.start();
t2.start();
}
}
  1. The above code adopts a locking sequence that is very easy to deadlock. That is, first let t1 get lock1, then let t2 get lock2, and then make a reverse request to let t1 apply for lock2 and t2 apply for lock1. In general, this will cause tl and t2 to wait for each other, resulting in deadlock.
  2. However, this situation is greatly improved by using the tryLock() method. Because the thread will not wait foolishly, but keep trying. Therefore, as long as the execution is long enough, the thread will always get all the required resources for normal execution (here, the thread obtains lock1 and lock2 locks at the same time as the conditions for normal execution). After obtaining lock1 and lock2 at the same time, the thread prints the message "My Jobdone" indicating the completion of the task.
  3. Execute the above code and wait for a while (because the thread contains sleep 500)
    Millisecond code). Finally, you can still be glad to see that the program is executed and produce the following output, indicating that both threads execute normally.

3, Fair lock

  • In most cases, lock applications are unfair. That is, thread 1 first requests lock a, and then thread 2 also requests lock a. So when lock a is available, can thread 1 get the lock or thread 2 get the lock? This is not necessarily true. The system will just randomly select one from the waiting queue of this lock. Therefore, its fairness cannot be guaranteed. This is like buying a ticket without queuing. Everyone is gathered in front of the ticket window. The conductor is too busy to care who comes first and who comes later. It's over when he finds someone to issue a ticket. The fair lock is not like this. It will ensure that the first to arrive first and the last to arrive later according to the order of time. A major feature of fair lock is that it will not produce hunger. As long as you line up, you can finally wait for resources. If we use the synchronized keyword for lock control, the generated lock is unfair. The reentry lock allows us to set its fairness. Its constructor is as follows:

  • When the parameter fair is
    true indicates that the lock is fair. Lock implementation requires fair system performance, but lock implementation requires fair system performance. Therefore, lock implementation requires fair system performance. If there are no special requirements, there is no need to use a fair lock. Fair locks and unfair locks are also very different in thread scheduling performance. The following code can highlight the characteristics of fair lock:
    Insert picture description here


    Line? Of the above code specifies that the lock is fair. Then, the t1 and t2 threads request the lock respectively, and after obtaining the lock, a console output is performed to indicate that they have obtained the lock. In the case of fair lock, the output is usually as follows:

    Since the code will produce a large amount of output, only the intercepted part is described here. In this output, it is obvious that two threads basically obtain locks alternately, and it is almost impossible for the same thread to obtain locks multiple times in succession, so as to ensure fairness. If a fair lock is not used, the situation will be completely different. Here are some outputs when a non fair lock is used:


    It can be seen that according to the scheduling of the system, a thread will tend to obtain the lock it has held again. This allocation method is efficient, but there is no fairness. Several important methods of ReentrantLock are summarized as follows.
  • lock(): obtain the lock. If the lock has been occupied, wait.
  • lockInterruptibly(): obtain the lock, but give priority to responding to the interrupt.
  • tryLock(): try to obtain the lock. If it succeeds, it returns true. If it fails, it returns false. The method returns immediately without waiting.
  • tryLock(long time, TimeUnit unit): try to obtain a lock within a given time. unlock(): release the lock.

In terms of the implementation of reentry lock, it mainly focuses on the Java level. The implementation of reentry lock mainly includes three elements.

  1. First, the atomic state. Atomic state uses CAS operation (discussed in detail in Chapter 4) to store the state of the current lock and judge whether the lock has been held by other threads.
  2. Second, wait queue. All threads that do not request locks will enter the waiting queue to wait. After a thread releases the lock, the system can wake up a thread from the waiting queue and continue to work.
  3. Third, the blocking primitives park() and unpark() are used to suspend and resume threads. Threads that do not get locks will be suspended. For details on park() and unpark(), please refer to section 3.1.7 thread blocking tool class: LockSupport.

From JAVA high concurrency programming, recommended

Keywords: Java Back-end

Added by mindrage00 on Tue, 15 Feb 2022 14:51:09 +0200