Java thread in-depth learning

Posted by nut legend on Tue, 21 Sep 2021 06:30:40 +0200

catalogue

Chapter 3 thread synchronization

3.1 thread synchronization mechanism

3.2 lock

3.2.1 function of lock

3.2.2 lock related concepts

3.3 internal lock: synchronized keyword

3.3.1 synchronized code block

3.4 lightweight synchronization mechanism: volatile keyword

3.4.1 role of volatile

3.4.2 volatile nonatomic properties

3.4.3 auto increment and auto decrement are performed for common atomic classes

3.5 CAS

3.6 atomic variable class

3.6.1 AtomicLong

3.6.2 AtomicIntegerArray

3.6.3 AtomicIntegerFieldUpdater

3.6.4 AtomicReference

Chapter 3 thread synchronization

3.1 thread synchronization mechanism

    Thread synchronization mechanism is a mechanism for coordinating data access between threads, which can ensure thread safety. The thread synchronization mechanism provided by the Java platform includes lock, volatile keyword, final keyword, static keyword and related API s, such as Object.wait(), Object.notify().

3.2 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 reuse this idea to ensure thread safety.

    Lock can be understood as a license to protect shared data. For shared data protected by the same lock, any thread that wants to access these shared data must hold the lock first. A thread can access these shared data only when it holds the lock. And a lock can only be held by one thread at a time. A thread that owns a lock must release the lock it holds after ending access to shared data.

    A thread must obtain a lock before accessing shared data. The thread that obtains the lock is called the lock holding thread; A lock can only be held by one thread at a time. The code executed by the lock holding thread after obtaining the lock and before releasing the lock is called critical section.

    Locks are exclusive, that is, a lock can only be held by one thread at a time. Such locks are called exclusive locks or Mutex locks.

    JVM divides locks into internal locks and display locks: internal locks are implemented through the synchronized keyword; The display lock is implemented through the implementation class of the java.concurrent.locks.Lock interface.

3.2.1 function of lock

    Locks can achieve secure access to shared data and ensure the atomicity, visibility and order of threads.

    Locks guarantee atomicity through mutual exclusion. A lock can only be held by one thread, which ensures that the code in the critical area can only be executed by one thread at a time, so that the operations performed by the code in the critical area naturally have the characteristics of indivisibility, that is, atomicity.

    The visibility is guaranteed by two actions: the write thread flushing the processor's cache and the read thread refreshing the processor's cache. In the Java platform, the acquisition of the lock implies the action of refreshing the processor cache, and the release of the lock implies the action of flushing the processor cache.

    The lock can ensure the order. The actions performed by the write thread in the critical area seem to be performed in full accordance with the source code order in the critical area performed by the read thread.

    Note: to use locks to ensure thread security, the following conditions must be met:

        a. These threads must use the same lock when accessing shared data;

        b. Even threads reading shared data need to use synchronization locks.

3.2.2 lock related concepts

    ① Reentrant reentrance: describes the problem of whether a thread can apply for the lock again (multiple times) when it holds the lock. 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.

        For example:

void methodA{
    Apply for lock here A
    call methodB();
    Release lock here A
}

void methodB{
    Apply for lock here A
    Execute action
    Release lock here A
}

        If a thread invokes the method B after obtaining the lock A, the lock A execution action can still be obtained in the method B, which indicates that the lock is available.

       Reentrant. If the application is unsuccessful, it cannot be re entered.

    ② Lock contention and scheduling. The internal lock in Java platform belongs to unfair lock; Show that lock supports both fair and non fair locks.

    ③ Granularity of locks. Granularity refers to the number and size of shared data that can be protected by a lock. If the shared data protected by the lock is large, the granularity of the lock is coarse, otherwise it is fine. The coarse granularity of the lock will cause the thread to wait unnecessarily when applying for the lock; Too fine granularity of locks will increase the cost of lock scheduling.

3.3 internal lock: synchronized keyword

    Each object in Java has an internal lock associated with it. This lock is also called monitor. This internal lock is an exclusive lock that can ensure atomicity, visibility and order.

    The internal lock is implemented through the synchronized keyword. The synchronized keyword modifies the code block and the method. The syntax format is:

        Synchronized (object lock){

                Sync code block, where you can access shared data

        }

    The modified instance method is called synchronous instance method; the modified static method is called synchronous static method.

3.3.1 synchronized code block

public class Test01{
    public static void main(String[] args){
        //Create two threads to print numbers separately
        Test01 obj = new Test01();
        new Thread(new Runnable(){
            public void run(){
                obj.mm();//The lock object is now the obj object
            }
        }).start();
        //Call the mm() method of the same object
        new Thread(new Runnable(){
            public void run(){
                obj.mm();//At this time, the obj object is occupied by the first thread. When it calls the mm() method to release the lock, the second thread can get the mm() method it started to execute. It has been waiting in the waiting area before
            }
        }).start();

        //The synchronization mechanism occurs only when the locks required by both threads are obj
    }
    
    //Print numbers
    publci void mm(){
        synchronized(this){//Use the current object as a lock
            for(int i = 0;i < 100;i++){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }
}

    In this example, if you do not lock, you cannot execute loops separately, and the loop results will be messy.

    If the thread needs different locks, the synchronization mechanism will not be implemented.

    You can use constants as lock objects because constants are also shared by objects created by this class.

public class Test01{
    public static void main(String[] args){
        //Create two threads to print numbers separately
        Test01 obj = new Test01();
        new Thread(new Runnable(){
            public void run(){
                obj.mm();
            }
        }).start();

        Test01 obj2 = new Test01();
        new Thread(new Runnable(){
            public void run(){
                obj2.mm();
            }
        }).start();

        //Although there are two objects obj and obj2, the two objects share an obj constant
        
        //Using the same lock object but different methods will also achieve synchronization
        new Thread(new Runnable(){
            public void run(){
                Test01.mm2();
            }
        }).start();
    }
    
    public static final Object OBJ = new Object();
    
    public void mm(){
        synchronized(OBJ){//Constant as lock
            for(int i = 0;i < 100;i++){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }

    public static void mm1(){
        synchronized(OBJ){//Constant as lock
            for(int i = 0;i < 100;i++){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }
}

    Use synchronized to modify the instance method, and this is the default lock object.

public class Test01{
    public static void main(String[] args){
        //Create two threads to print numbers separately
        Test01 obj = new Test01();
        new Thread(new Runnable(){
            public void run(){
                obj.mm();
            }
        }).start();

        new Thread(new Runnable(){
            public void run(){
                obj.mm();
            }
        }).start();
    }
    
    public synchronized void mm(){//Modify the instance method. this is the lock object by default
        for(int i = 0;i < 100;i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }

    public synchronized void mm2(){
        synchronized(this){//this is the lock object
            for(int i = 0;i < 100;i++){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }
}

    synchronized modifies a static method. The default lock object is the runtime class object of the current class, also known as class lock.

public class Test01{
    public static void main(String[] args){
        //Create two threads to print numbers separately
        Test01 obj = new Test01();
        new Thread(new Runnable(){
            public void run(){
                mm();
            }
        }).start();
        
        new Thread(new Runnable(){
            public void run(){
                obj.mm2();
            }
        }).start();
    }
    
    public synchronized static void mm(){//Modifies a static method. The default runtime class is a lock object
        for(int i = 0;i < 100;i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }

    public synchronized void mm2(){
        synchronized(Test01.class){//The current class object is a lock object
            for(int i = 0;i < 100;i++){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }
}

    The granularity of synchronization method lock is coarse and the execution efficiency is low; Synchronous code block lock has high granularity and high execution efficiency. Generally, synchronous code block lock is selected.

    If an exception occurs in the thread during synchronization, the lock object will be automatically released.

    Deadlock: in multithreaded programs, multiple locks may be used during synchronization. If the order of obtaining locks is inconsistent, it may lead to deadlock. For example, thread a needs to obtain lock 1 first and then lock 2; Thread B obtains lock 2 first, and then lock 1. If thread a obtains lock 1 first and thread B obtains lock 2 first, thread a will wait for lock 2 of thread B and thread B will wait for lock 1 of thread A. that lock cannot be released and neither lock can be obtained, it will fall into a deadlock.

    How to avoid deadlock? When multiple locks are required, all threads can obtain locks in the same order.

3.4 lightweight synchronization mechanism: volatile keyword

3.4.1 role of volatile

    The key is to make variables visible between multiple threads. The thread can be forced to read the value of the variable from the public memory instead of from the working memory, so as to realize the visibility between multiple memories.

public class Test02{
    public static void main(String[] args){
        
        PrintString printString = new PrintString();
        //Create a child thread   
        new Thread(new Runnable(){
            public void run(){
                printString.printStringMethod();
            }
        })
        
        //You can add a sleep in the middle to make the effect more obvious
        printString.setContinuePrint(false);//If the volatile modifier is not used, the child thread may not read the flag, and the position is false
    }
}

class PrintString{
    private volatile boolean continuePrint = true;//volatile forces the thread to read the value of a variable from public memory

    public PringString setContinuePrint(boolean continuePrint){

        this.continuePrint = continuePrint;
        return this;
    } 

    public void printStringMethod(){
        while(continuePrint){
        }
    }
}

    volatile vs synchronized:

        ① Volatile keyword is a lightweight implementation of thread synchronization, so volatile performance is certainly better than synchronized; along with

          With the release of the new version of JDK, the execution efficiency of synchronized has also been greatly improved. Synchronized is used in development

          The ratio is still large.

        ② volatile can only modify variables, while synchronized can modify methods and code blocks.

        ③ Multithreaded access to volatile variables does not block, while synchronized may block.

        ④ volatile can guarantee the visibility of data, but it cannot guarantee atomicity; synchronized ensures atomicity, and

          Visibility is guaranteed.

        ⑤ volatile solves the visibility of variables among multiple threads; The synchronized keyword resolves access between multiple threads

          Ask about the synchronization of public resources.

3.4.2 volatile nonatomic properties

    volatile keyword increases the visibility of instance variables among multiple threads, but it is not atomic.

3.4.3 auto increment and auto decrement are performed for common atomic classes

    We know that i + + is not an atomic operation. In addition to using synchronized for synchronization, it can also be implemented using AtomicInteger/AtomicLong atomic classes.

public class Test{
    public static void main(String[] args){
        for(int i = 0;i < 10;i++){
            new MyThread().start();
        }
        
        System.out.println(MyThread.count.get());//The number read here must be 1000 * 10 = 10000, because the self increment at this time has atomicity
    } 
}

class MyThread extends Thread{
    //Using AtomicInteger objects
    private static AtomicInteger count = new atomicInteger();

    public static void addCount(){
        for(int i = 0;i < 1000;i++){
            count.getAndIncrement();//Indicates the form of self increasing suffix
        }
    } 

    public void run(){
        addCounr();
    }
}

3.5 CAS

    CAS(Compare And Swap) is implemented by hardware. CAS can convert operations such as read modify write into atomic operations. For example, i + + auto increment operation includes three sub operations: reading the value of i variable from main memory; Add 1 to the value of i; Then save the value after adding 1 to main memory.

    CAS principle: before updating the data to the main memory, read the value of the main memory variable again. If the value of the variable is the same as the expected value (the value read at the beginning of the operation), it will be updated. If not, cancel (or redo) this operation.  

    Use CAS to implement a thread safe counter:

public class CASTest{
    public static void main(String[] args){
        CASCounter casCounter = new CASCounter();

        for(int i = 0;i < 1000;i++){
            new Thread(new Runnable(){
                public void run(){
                    System.out.println(casCounter.incrementAndGet());
                }
            }).start();
        }
    }
}

class CASCounter{
    //volatile modifies value so that the value is visible to all threads
    volatile private long value;

    public long getValue(){
        return value;
    }

    //Define campare and swap
    private boolean compareAndSwap(long expectedValue,long newValue){
        //If the current value is the same as the expected expected value, replace the current value field with newValue
        //The replacement process should be synchronized
        synchronized(this){
            if(value == expectedValue){//If the currently read value is the same as the expected value (the value read before the increment 1 operation)
                value = newValue;//Assign new value
                return true;
            }else{
                return false;
            }
        }
    }

    //Define the method of self increment
    public long incrementAndGet(){
        long oldValue;
        long newValue;
        do{
            oldValue = value;//Currently read value
            newValue = oldValue + 1;//Add 1 to the currently read value
        }while(!(compareAndSwap(oldValue,newValue)));//If it does not return true, it will continue to loop
        return newValue;
    }
}

    There is an assumption behind the atomicity of CAS: the current value of the shared variable is the same as the expected value provided by the current thread, so it is considered that this variable has not been modified by other threads. In fact, this assumption does not necessarily stand.

        For example, there is a shared variable count=0. A current thread reads the value as 0 for the first time, and then wants to modify the value. During this period, a thread a modifies the count value to 10, and thread B modifies the count value to 0. After modification, the current thread again sees the count value as 0. Now do you think the value of the count variable has not been updated by other threads? Is this result acceptable?

    This is the ABA problem in CAS, that is, the shared variables are updated by a - > b - > a. Whether ABA can be accepted is related to the implemented algorithm. If you want to avoid ABA problems, you can introduce a revision number (timestamp) for the shared variable. Each time you modify the shared variable, the corresponding revision number will increase. At this time, the expected value and revision number form a tuple to judge whether the value before modification is consistent with the value after modification. ABA variable update process: [a, 0] - [b, 1] - [2], Each modification of a shared variable will lead to an increase in the revision number. Through the revision number, you can still accurately judge whether the variable has been modified by other threads. AtomicStampedReference class is based on this idea.

3.6 atomic variable class

    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, the internal class of atomic variable uses a volatile variable and ensures the atomicity of read modify write operation of changing quantity. Sometimes, the atomic variable class is regarded as an enhanced volatile variable. There are 12 atomic variable classes.

groupingAtomic variable class
Basic data typeAtomicInteger   AtomicLong   AtomicBoolean
Array typeAtomicIntegerArray   AtomicLongArray   AtomicReferenceArrray
Field UpdaterAtomicIntegerFieldUpdater   AtomicLongFieldUpdater   AtomicReferenceFieldUpdater   
Reference typeAtomicReference   AtomicStampedReference   AtomicMarkableReference

3.6.1 AtomicLong

    Define a counter using AtomicLong.

//Simulate and count the total number of requests, processing successes and processing failures of the server
public class Test{
    public static void main(String[] args){
        //Through the thread simulation request, in practical application, the relevant method of calling the counter can be invoked in the filter.
        for(int i = 0;i < 1000;i++){
            new Thread(new Runnable(){
                public void run(){
                    Indicator.getInstance().newRequestReceive();
                    int num = new Random.nextInt();
                    if(num % 2 == 0){
                        Indicator.getInstance().requestProcessSuccess();
                    }else{
                        Indicator.getInstance().requestProcessFailure();
                    }
                }
            })
        }
    }
}

//Define a counter using an atomic variable class
class Indicator{
    //Construction method privatization
    private Indicator(){
    }
    //Define a private static object of this class, so that the same counter is used wherever it is used
    private static final Indicator INSTANCE = new Indicator();
    //Provide a public static method that returns a unique instance of the class
    public static Indicator getInstance(){
        return INSTANCE;
    }
    
    //The atomic variable class is used to save the total number of requests, successes and failures
    private final AtomicLong requestCount = new AtomicLong(0);
    private final AtomicLong successCount = new AtomicLong(0);
    private final AtomicLong failureCount = new AtomicLong(0);

    //There are new requests
    public void newRequestReceive(){
        requestCount.incrementAndGet();
    }
    //Processing succeeded
    public void requestProcessSuccess(){
        successCount.incrementAndGet();
    }
    //Processing failed
    public void requestProcessFailure(){
        failureCount.incrementAndGet();
    }
    
    //View the total number, successes and failures
    public long getRequestCount(){
        return requestCount;
    }
    public long getSuccessCount(){
        return SuccessCount;
    }
    public long getFailureCount(){
        return failureCount;
    }
}

3.6.2 AtomicIntegerArray

    Atomic update array.

public class Test{
    public static void main(String[] args){
        //Creates an atomic array of the specified length
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray();
        
        //Returns the element at the specified location
        System.out.println(atomicIntegerArray.get(2));
        
        //Sets the element at the specified location
        atomicIntegerArray.set(0,10);
        
        //When setting the element of the specified subscript of the array, the old value of the array element is returned at the same time
        Systemout.println(atomicIntegerArray.getAndSet(1,11));
        
        //Modify the value of the array element and add a value to the array element
        System.out,println(atomicIntegerArray.addAndGet(0,22));
        System.out,println(atomicIntegerArray.getAndAdd(1,33));
        
        //CAS operation
        //If the value of the element with index value 0 in the array is 32, it is modified to 222
        System.out,println(atomicIntegeyArray.compareAndSet(0,32,222));

        //Self increasing / self decreasing
        System.out.println(atomicIntegerArray.incrementAndGet(0));//++i
        System.out.println(atomicIntegerArray.getAndIncrement(0));//i++
        System.out.println(atomicIntegerArray.decrementAndGet(0));//--i
        System.out.println(atomicIntegerArray.getAndDecrement(0));//i--
    }
}

3.6.3 AtomicIntegerFieldUpdater

    The atomic integer field can be updated. It is required that:

        ① Characters must be decorated with volatile to make them visible between threads;

        ② It can only be an instance variable, not a static variable, nor can it be decorated with final.

    Is an abstract class.

public class Test{
    public static void main(String[] args){
        User user - new User(1234,10);
        
        //Open ten threads
        for(int i = 0;i < 10;i++){
            new SubThread(user).start();
        }
        
        Thread.sleep(1000);//Exception handling
        
        System.out.println(user);
    }
}


class User{
    int id;
    volatile int age;
    
    public User(int id,int age){
        this.id = id;
        this.age = age;
    }

    //Override toString method
}


class SubThread extends Thread{
    private User user;
    //Create AtomicIntegerFieldUpdater Updater
    private AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
    
    public SubThread(User user){
        this.user = user;
    }

    public void run(){
        //Increase the age field of the user object by 10 times in the child thread
        for(int i = 0;i < 10;i++){
            System.out.println(updater.getAndIncrement(user));
        }
    }
}

3.6.4 AtomicReference

    You can read and write an object atomically.

public class Test{
    private static AtomicReference<String> atomicReference = new AtomicReference<>("abc");

    public static void main(String[] args){
        //Create 100 threads to modify the string
        for(int i= 0;i < 100;i++){
            new Thread(ne Runnable(){
                public void run(){
                    if(atomicReference.compareAndSet("abc","def")){
                        System.out.println(Thread.currrentThread().getName() + "Put string abc Change to def");
                    }
                }
            })
        }

        //Create another 100 threads
        for(int i= 0;i < 100;i++){
            new Thread(ne Runnable(){
                public void run(){
                    if(atomicReference.compareAndSet("def","abc")){
                        System.out.println(Thread.currrentThread().getName() + "Put string def Revert to abc");
                    }
                }
            })
        }
        
        Thread.sleep(1000);
        System.out.println(atomicReference.get());
    }
}

    The AtomicReference may have ABA problems with CAS.

public class Test{
    private static AtomicReference<String> atomicReference = new AtomicReference<>("abc");

    public static void main(String[] args){
        Thread t1 = new Thread(new Runnable(){
            public void run(){
                atomicReference.compareAndSet("abc","def");
                System.out.println(Thread.currentThread().getName() + "--" + atomicReference.get());
                atomicReference.compareAndSet("def","abc");
            }
        })

        Thread t2 = new Thread(new Runnable(){
            public void run(){
                Thread.sleep(1000);
                atomicReference.compareAndSet("abc","ghg");
            }
        })

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(atomicReference.get());
    }
}

    The ABA problem in CAS can be solved by using AtomicStampedReference and AtomicMarkedReference. There is an integer tag value stamp in AtomicStampedReference atomic class. Each time CAS operation is performed, its version needs to be compared, that is, the value of stamp needs to be compared.

public class Test{
    private static AtomicReference<String> atomicReference = new AtomicReference<>("abc");
    private static AtomicStampedReference<String> stampedReference = new AtomicStampedReference("abc",0);//The version number starts from 0

    public static void main(String[] args){
        Thread t1 = new Thread(new Runnable(){
            public void run(){
                stampedReference.compareAndSet("abc","def",stampedReference.getSamp(),stampedReference.getSamp() + 1);
                System.out.println(Thread.currentThread().getName() + "--" + atomicReference.getReference());                
                stampedReference.compareAndSet("def","abc",stampedReference.getSamp(),stampedReference.getSamp() + 1);
            }
        })

        Thread t2 = new Thread(new Runnable(){
            public void run(){
                int stamp = stampedReference.getSamp();//Get the version number first
                Thread.sleep(1000);//During sleep, thread 1 has been modified and the version number has changed
                stampedReference.compareAndSet("abc","ghg",stamp,stamp + 1);//This causes the modification to fail at this time
            }
        })

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(stampedReference.getReference);
    }
}

PS: sort out according to the power node course. If there is infringement, contact to delete it.

Topics: Java Multithreading