Multithreading (synchronous lock, optimistic lock, pessimistic lock, fair lock, reentrant lock, deadlock, spin lock, read / write lock)

Posted by eagleweb on Sat, 18 Dec 2021 20:13:44 +0100

Multithreading

process

A process is a running program

  • It is an independent unit where the system allocates and invokes resources
  • Each process has its own memory space and system resources

thread

Thread: it is a single sequential control flow in a process and an execution path

  • Single thread: if a process has only one execution path, it is called a single threaded program
  • Multithreading: if a process has multiple execution paths, it is called a multithreaded program

Common methods of multithreading

  • The run method is used to encapsulate the code executed by the thread

  • run(): encapsulates the code executed by the thread. It is called directly, which is equivalent to the call of ordinary methods

  • start(): start the thread; The run() method of this thread is then called by the JVM

  • Thread.currentThread(): get that any piece of java code of the current thread is executed in a thread, and the return value of the method is the thread object when the code is actually running.

  • isAlive(): judge whether the current thread is still alive

  • Thread.sleep(millis): lets the current thread sleep for the specified number of milliseconds. The current thread refers to thread The thread returned by currentthread()

  • Thread. The yield () method is used to yield, not necessarily yield. Success depends on your mood

  • thread.setPriority(num): sets the priority of threads. The value range of num is 1 to 10. The default value is 5 num. if it exceeds the range, an exception will be thrown. Threads with high priority have A high probability of obtaining cpu time slices. Who executes first or depends on the cpu mood. Improper setting may cause some threads to never run, so the higher the thread priority is not set, the better. Generally, the ordinary priority can be used. The priority of thread is inherited. If thread B is created in thread A, the priority of thread B is the same as that of thread A.

  • Interrupt(): interrupting a thread only marks a stop flag on the current thread. It does not really stop the thread. Call this method outside of a.interrupt() this method in a Isinteropted() this method can return the interrupt flag of the thread. The returned value of a is true. When the thread is in the wait() waiting state, calling the interrupt () method of the thread object will interrupt the waiting state of the thread and generate an InterruptedException exception.

  • setDaemon(true): set daemon threads. Threads in java are divided into user threads and daemon threads. Daemon threads are threads that provide services for other threads. For example, garbage collector (GC) is a typical daemon thread. When all user threads are executed, the daemon thread will be automatically destroyed

  • Notify() and notifyAll(): notify() can only wake up one thread at a time. If there are multiple waiting threads, only one of them can be awakened randomly; To wake up all waiting threads, you need to call notifyAll()

  • wait(long) wait(5000) milliseconds with long type parameter. If it is not awakened within the time specified by the parameter, it will wake up automatically after timeout

  • getState(): this method can get the state of the thread.

    • NEW, create a NEW state, create a thread object, and the state before calling the start method to start
    • RUNNABLE, RUNNABLE state. It is a composite state, including two states: READY and RUNNING. The READY state can be scheduled by the thread scheduler to make it in the RUNNING state. The RUNNING state indicates that the thread is executing. Thread. The yield () method converts the thread from RUNNING state to READY state.
    • BLOCKED status: when a thread initiates a BLOCKED IO operation or applies for exclusive resources occupied by other threads, the thread will be converted to BLOCKED status. The thread in BLOCKED status will not occupy CPU resources. When the BLOCKED IO operation is completed or the thread obtains the resources it applies, the thread can be converted to RUNNABLE
    • WAITING state, the thread executed object wait(),thread. The join () method will convert the thread to WAITING state and execute object After the notify () method or the added thread is executed, the current thread will change to the RUNNABLE state
    • TIMED_ The WAITING state is similar to the WAITING state. The difference is that the thread in this state will not wait indefinitely. If the thread fails to complete the desired operation within the specified time, the thread will be automatically converted to RUNNABLE
    • TERMINATED state, the thread ends.

Thread safety

It is manifested in three aspects: atomicity, visibility and order

Atomicity

Atomic means indivisible

  1. The operation of accessing (reading, writing) a shared variable. From the perspective of other threads, the operation has either completed or has not yet occurred, that is, other threads show the intermediate results of the current operation

  2. Atomic operations that access the same set of shared variables cannot be interleaved

Java has two ways to achieve atomicity: one is to use locks; The other uses the CAS(Compare and Swap) instruction of the processor

The atomic variable class is implemented based on CAS. When read modify write updates are performed on shared variables, the atomicity and visibility of the operation can be guaranteed through the atomic variable class The read modify write update operation of a variable means that the current operation is not a simple assignment, but the new value of the variable depends on the old value of the variable, such as the self increment operation i + + Because volatile can only guarantee visibility and cannot guarantee atomicity, a volatile variable is used inside the atomic variable class, and the atomicity of the read modify write operation of the variable is guaranteed. Sometimes, the atomic variable class is regarded as an enhanced volatile variable There are 12 atomic variable classes, such as:

visibility

In a multithreaded environment, after a thread updates a shared variable, subsequent threads may not be able to immediately read the updated results. This is another form of thread safety problem: visibility

If a thread updates a shared variable and other threads accessing the variable can read the update result, it is said that the thread's update of the shared variable is visible to other threads; otherwise, it is said that the thread's update of the shared variable is invisible to other threads Multithreaded programs may cause other threads to read old data (dirty data) because of visibility problems

Order

The order of code written is not necessarily the order of execution; It will be affected by the compiler and cpu. In order to optimize the execution rate of the code, the java compiler will change the execution order of the code. Of course, this is on the premise of ensuring a single thread. Therefore, there is a thread safety problem in multithreading.

java virtual machine memory model: understanding the causes of thread insecurity

Thread working memory is a model abstracted by JMM. First, variables are stored in main memory. Each thread has its own independent working memory, which stores a copy of the variables used by the thread. The value modified by a thread is a copy of the working memory of the thread, and then the modified value is refreshed to main memory.

JMM stipulates that all operations of threads on shared variables must be carried out in their own working memory, and cannot be read and written directly in the main memory. Variables in the working memory of other threads cannot be accessed directly between different threads. Variable values between threads can only be transferred through memory. Untimely updates lead to thread safety problems.

lock

The premise of thread safety problem is that multiple threads access shared data concurrently The concurrent access of multiple threads to shared data is transformed into serial access, that is, a shared data can only be accessed by one thread at a time Lock is to use this idea to ensure thread safety

Reentrant lock

Reentrancy describes the problem that when a thread holds the lock, it can apply for the lock again (multiple times). If a thread can continue to successfully apply for a lock when it holds it, it is said that the lock is reentrant, otherwise it is said that the lock is non reentrant.

void methodA(){
      apply a lock
      methodB();
      release a lock
}
void methodB(){
      apply a lock
....  release a lock
}

Lock acquisition and scheduling

Internal locks in the Java platform are unfair locks. It shows that Lock locks support both fair and unfair locks

Lock granularity

The number of shared data that a lock can protect is called lock granularity Lock protection has a large amount of shared data. Call the lock coarse granularity as a heavyweight lock. Otherwise, call the lock fine granularity as a lightweight lock The coarse grain of lock will cause the thread to wait unnecessarily when applying for lock Too fine granularity of locks will increase the cost of lock scheduling

Internal lock

Every object in Java has an internal lock associated with it This lock is also called monitor. This internal lock is an exclusive lock, which can ensure atomicity, visibility and order The internal lock is implemented through the synchronized keyword The synchronized keyword modifies the code block and modifies the method

Syntax for decorating code blocks:

Synchronized (object lock){

Sync code block, where you can access shared data}

The modified instance method is called the synchronous instance method (this)

A modified static method is called a synchronous static method (class. clss)

deadlock

public class T3 {
    final Object locka = new Object();
    final Object lockb = new Object();

    public void m1() {
        String tn = Thread.currentThread().getName();
        System.out.printf("%s Start waiting%n", tn);
        synchronized (locka) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            synchronized (lockb) {
            }
        }
        System.out.printf("%s end%n", tn);
    }

    public void m2() {
        String tn = Thread.currentThread().getName();
        System.out.printf("%s Start waiting%n", tn);
        synchronized (lockb) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            synchronized (locka) {
            }
        }
        System.out.printf("%s end%n", tn);
    }

    public static void main(String[] args) {
        var t = new T3();
        new Thread(t::m1, "A").start();
        new Thread(t::m2, "B").start();
    }
}

Comparison between volatile and synchronized

  1. Volatile keyword is a lightweight implementation of thread synchronization, so volatile performance is certainly better than synchronized; Volatile can only modify variables, while synchronized can modify methods and code blocks With the release of the new version of JDK, the execution efficiency of synchronized has also been greatly improved, and the rate of using synchronized in development is still very large
  2. Multithreaded access to volatile variables does not block, while synchronized may block
  3. volatile can guarantee the visibility of data, but it cannot guarantee atomicity; synchronized ensures atomicity and visibility
  4. The keyword volatile solves the visibility of variables among multiple threads;

Atomic understanding

If the code of the thread run method is as follows, a is a shared variable. Three operations are required for two threads to execute the shared variable. Atomicity is that these three operations are integrated and cannot be separated. They will not be disturbed when running. Adding volatile can only refresh the first copy, so atomicity cannot be guaranteed.

  1. Obtain copy a=0// When t2 grabs the thread and t1 grabs it back, the second part will be overwritten directly, resulting in the a variable not being reversed
  2. Then a=0;
  3. Synchronous main memory
while(true){
    a++;
}

atomic classes can solve this problem

int -AtomicInteger spin lock

long-AtomicLong

boolean-AtomicBollean

Atomicreference resolves references to atomic classes

Optimistic lock: it only aims at the modification of the value, and only adds verification at the modification. It doesn't care about the specific large code logic

Pessimistic lock: for large sections of logic and context related, it is necessary to turn the code into atomicity. For more changes, less checks.

CAS

CAS (Compare And Swap) is implemented by hardware CAS can convert operations such as read- modify - write into atomic operations

i + + auto increment operation includes three sub operations:

  • Read i variable value from main memory

  • Add 1 to the value of i

  • Then save the value after adding 1 to main memory

CAS principle: when updating the data to the main memory, read the value of the main memory variable again. If the current value of the variable is the same as the expected value (the value read at the beginning of the operation), it will be updated

Communication between threads

Implementation of waiting notification mechanism

The wait() method in the Object class can make the thread executing the current code wait and pause until it is notified or interrupted Note: 1) the wait() method can only be called by the lock Object in the synchronization code block. 2) call the wait() method, and the current thread will be released

The notify () of the Object class can wake up the thread, and this method must also be called by the lock Object in the synchronization code block Calling wait()/notify() without a lock Object throws an illegalmonitorstateexception If there are multiple waiting threads, the notify () method can wake up only one of them When the notify () method is invoked in the synchronous code block, the lock Object is not released immediately, and the lock Object is released until the current synchronization block is executed. The notify () method is usually placed at the end of the synchronized block. Its pseudo code is as follows

Lock display lock

The Lock interface is added in JDK5, with ReentrantLock implementation class. ReentrantLock Lock is called reentrant Lock, which has more functions than synchronized

Reentrant lock ReentrantLock

If a method a needs this lock, a method B also needs this lock. If the current thread can obtain the lock again, this is the reentrancy of the lock. If it is not reentrant, it may cause deadlock

  • Call the lock() method to obtain the lock,

  • Call unlock() to release the lock

  • The lockInterruptibly() method is used to obtain a lock if the current thread is not interrupted, and an exception occurs if the current thread is interrupted. For synchronized internal locks, if a thread is waiting for a lock, there are only two results: either the thread obtains the lock and continues to execute; Or just keep waiting For ReentrantLock, another possibility is provided. During the process of waiting for the lock, the program can cancel the request for the lock according to the needs of this method.

  • tryLock(long time, TimeUnit unit) is used to obtain the lock if the lock is not held by another thread within a given waiting time and the current thread is not interrupted Through this method, the time limited waiting of the lock object can be realized. lock.tryLock() only tries and is occupied by other threads. It returns false and does not wait.

tryLock solves deadlock problems

package com.ysh;

import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @className: com.ysh.ThreadDemoabc
 * @description: TODO
 * @author: YSH
 * @create: 2021-08-27 11:38
 */
public class ThreadDemoabc {

    static class IntLock implements Runnable {
        private static ReentrantLock lock1 = new ReentrantLock();
        private static ReentrantLock lock2 = new ReentrantLock();
        private int lockNum; //Used to control the sequence of locks

        public IntLock(int lockNum) {
            this.lockNum = lockNum;
        }

        @Override
        public void run() {
            if (lockNum % 2 == 0) { //Even numbers lock 1 first and then 2
                while (true) {
                    try {
                        if (lock1.tryLock()) {
                            System.out.println(Thread.currentThread().getName() + "Acquire lock 1, Also want to get lock 2");
                            Thread.sleep(new Random().nextInt(100));
                            try {
                                if (lock2.tryLock()) {
                                    System.out.println(Thread.currentThread().getName() + "Obtain lock 1 and lock 2 at the same time ----It's done");
                                    return; //End the execution of the run() method, that is, the current thread
                                }
                            } finally {
                                if (lock2.isHeldByCurrentThread()) {
                                    lock2.unlock();
                                }
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        if (lock1.isHeldByCurrentThread()) {
                            lock1.unlock();
                        }
                    }
                }
            } else { //For odd numbers, lock 2 first and then 1
                while (true) {
                    try {
                        if (lock2.tryLock()) {
                            System.out.println(Thread.currentThread().getName() + "Acquire lock 2, Also want to get lock 1");
                            //Thread.sleep(new Random().nextInt(100))
                            try {
                                if (lock1.tryLock()) {
                                    System.out.println(Thread.currentThread().getName() + "Obtain lock 1 and lock 2 at the same time ----It's done");
                                    return; //End the execution of the run() method, that is, the current thread

                                }
                            } finally {
                                if (lock1.isHeldByCurrentThread()) {
                                    lock1.unlock();
                                }
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        if (lock2.isHeldByCurrentThread()) {
                            lock2.unlock();
                        }
                    }
                }
            }
        }


        public static void main(String[] args) {
            IntLock intLock1 = new IntLock(11);
            IntLock intLock2 = new IntLock(22);
            Thread t1 = new Thread(intLock1);
            Thread t2 = new Thread(intLock2);
            t1.start();
            t2.start();
//After running, use tryLock() to try to obtain the lock. You won't wait foolishly and try again through a loop. If
            // Wait long enough for the thread to always get the desired resources
        }
    }
}

Condition

The keyword synchronized is used with wait()/notify() to implement the wait / notify mode The newcondition () method of Lock returns the Condition object, and the Condition class can also implement the wait / notify mode When using notify() notification, the JVM wakes up a waiting thread randomly The Condition class can be used for selective notification Two commonly used methods of Condition: await() will make the current thread wait and release the Lock. When other threads call signal(), the thread will regain the Lock and continue to execute Signal () is used to wake up a waiting thread. Note: before calling the await()/signal() method of Condition, the thread also needs to hold the relevant Lock lock After await() is called, the thread will release the Lock. After singal() is called, a thread will be awakened from the waiting queue of the current Condition object. The awakened thread attempts to obtain the Lock. Once the Lock is obtained successfully, it will continue to execute

private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
lock.lock();
conditionA.await();
lock.unlock();
//-----------
lock.lock();
conditionA.await();
lock.unlock();
//It is equivalent to assigning a name to each thread waiting for lock lock. At that time, you can directly select the thread you want to wake up
//Then wake up the thread you want to wake up with the main method
lock.lock();
conditionA.signal();
lock.unlock();
//------------
lock.lock();
conditionA.signal();
lock.unlock();

Fair lock and unfair lock

In most cases, lock applications are unfair If both thread 1 and thread 2 are requesting lock A, when lock A is available, the system will only randomly select A thread from the blocking queue, and its fairness cannot be guaranteed Fair locks will be in chronological order to ensure first come, first served. This feature of fair locks will not lead to thread starvation synchronized internal locks are unfair ReentrantLock provides A construction method of reentrant lock: ReentrantLock(boolean fair). When the lock object is created, the argument passes true to set the lock as A fair lock Fair lock looks fair, but to realize fair lock, the system must maintain an ordered queue. The implementation cost of fair lock is high and the performance is low Therefore, locks are unfair by default It is not A special requirement. Generally, fair locks are not used

Common methods of ReentrantLock

  • int getHoldCount() returns the number of times the lock() method was called by the current thread
  • int getQueueLength() returns the estimated number of threads waiting to acquire a lock
  • int getWaitQueueLength(Condition condition) returns the estimated number of threads waiting related to the Condition condition
  • boolean hasQueuedThread(Thread thread) queries whether the thread specified by the parameter is waiting for a lock
  • boolean hasQueuedThreads() queries whether there are threads waiting to acquire the lock
  • boolean hasWaiters(Condition condition) queries whether a thread is waiting for the specified Condition condition
  • boolean isFair() determines whether it is a fair lock
  • boolean isHeldByCurrentThread() determines whether the current thread holds the lock
  • boolean isLocked() queries whether the current lock is held by the thread

ReentrantReadWriteLock read / write lock

synchronized internal lock and ReentrantLock lock are exclusive locks (exclusive locks). Only one thread is allowed to execute synchronous code blocks at the same time, which can ensure thread safety, but the execution efficiency is low

ReentrantReadWriteLock is an improved exclusive lock, which can also be called shared / exclusive lock Multiple threads are allowed to read shared data at the same time, but only one thread is allowed to update shared data at a time

Read / write lock completes read / write operations through read lock and write lock A thread must hold a read lock before reading shared data. The read lock can be held by multiple threads at the same time, that is, it is shared Threads must hold write locks before modifying shared data. Write locks are exclusive. When a thread holds a write lock, other threads cannot obtain the corresponding lock

The read lock is only shared among the read threads. When any thread holds the read lock, other threads cannot obtain the write lock, so as to ensure that no other thread updates the data during data reading, so that the read thread can read the latest value of the data and ensure sharing during data reading

Read share read write mutex write mutex

//Define read / write lock
ReadWriteLock rwLock = new ReentrantReadWriteLock();
//Acquire read lock
Lock readLock = rwLock.readLock();
//Obtain write lock
Lock writeLock = rwLock.writeLock();
//Read data
readLock.lock(); //Apply for read lock rwlock readLock. lock()
try{
Read shared data
}finally{
readLock.unlock(); //Always release the lock in the finally clause
}
//Write data
writeLock.lock(); //Apply for write lock
try{
Updating and modifying shared data
}finally{
writeLock.unlock(); //Always release the lock in the finally clause
}

Thread pool

What is a thread pool

You can use new thread (() - > {task executed by thread}) start(); This form starts a thread When the run() method ends, the thread object will be released by GC

In a real production environment, many threads may be required to support the whole application. When the number of lines is very large, it will exhaust CPU resources If you do not control and manage threads, it will affect the performance of the program Thread overhead mainly includes:

The cost of creating and starting threads; Thread destruction overhead; Thread scheduling overhead; The number of threads is limited to the number of CPU processors Thread pool is a common way to use threads effectively A certain number of working threads can be created in the thread pool in advance. The client code directly submits tasks to the thread pool as an object. The thread pool caches these tasks in the work queue. The working threads in the thread pool constantly take out tasks from the queue and execute them

//Create a thread pool with 5 thread sizes,
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
//Submit 18 tasks to the thread pool, which are stored in the blocking queue of the thread pool
5 A thread fetches a task from the blocking queue for execution
for (int i = 0; i < 18; i++) {
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + " The numbered task is in execution
 Line task,start time: " + System.currentTimeMillis());
try {
          Thread.sleep(3000); //Simulation task execution duration
      } catch (InterruptedException e) {
               e.printStackTrace();
                                       }
            }
          });
   }
  }
}
/**
* Scheduled tasks for thread pool
*/
public class Test02 {
public static void main(String[] args) {
//Create a thread pool with scheduling function
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(10);
//Execute the task after 2 seconds delay, schedule (runnable task, delay length, time unit)
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
       System.out.println(Thread.currentThread().getId() + " -- " +
        System.currentTimeMillis() );
        }
}, 2, TimeUnit.SECONDS);
//Execute the task at a fixed frequency. The time to start the task is fixed. Execute the task after 3 seconds, and then re execute it every 5 seconds
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
         public void run() {
               System.out.println(Thread.currentThread().getId() + "----Open task at fixed frequency---" + System.currentTimeMillis());
                   TimeUnit.SECONDS.sleep(3); 
//Sleep simulates the task execution time. If the task execution time exceeds the time interval, the next task will be started immediately after the task is completed
                    }
}, 3, 2, TimeUnit.SECONDS);*/
//After the end of the last task, execute the task again after a fixed delay. No matter how long it takes to execute the task, always start a new task again 2 seconds after the end of the task
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
           @Override
          public void run() {
               System.out.println(Thread.currentThread().getId() + "----Open task at fixed frequency---" + System.currentTimeMillis());
TimeUnit.SECONDS.sleep(3); //Sleep simulates the task execution time if the task is executed
 The row length exceeds the time interval,Then start the next task immediately after the task is completed
} 

Topics: Java