JUC high concurrency programming: CAS, 6 locks

Posted by dlcmpls on Sun, 26 Dec 2021 22:09:55 +0100

CAS

summary

The full name of CAS is compare and swap, which is a CPU concurrency primitive,

  • Compare whether the working memory value (expected value) and the shared value of the main physical memory are the same. If they are the same, perform the specified operation. Otherwise, continue the comparison until the values of the main memory and the working memory are consistent. This process is atomic
  • AtomicInteger class mainly uses CAS(compare and swap)+volatile and native methods to ensure atomic operations, so as to avoid the high overhead of synchronized and greatly improve the execution efficiency

The CAS concurrency primitive embodied in the Java language is sun Each method in the UnSafe class under misc package Call the CAS method in the UnSafe class, and the JVM will help me implement the CAS assembly instruction

  • This is a completely hardware dependent function through which atomic operation is realized

  • CAS is a system primitive, which belongs to the category of operating system. It is a process composed of several instructions to complete a function, and the execution of the primitive must be continuous. Interruption is not allowed in the execution process, that is to say, CAS is an atomic instruction and will not cause the so-called problem of data inconsistency

  • Examples, such as the getAndIncrement method in unSafe

  • public final boolean compareAndSet(int expect, int update): if the current status value is equal to the expected value, set the synchronization status to the given update value in atomic mode.

	/*
	* CAS:Compare and swap [Compare and exchange]
	* */
	public class AtomicIntegerDemo {
	    public static void main(String[] args) {
	    	//The current status value is 5
	        AtomicInteger atomicInteger=new AtomicInteger(5);
	        //Current status value: 5; Expected value: 5, updated value: 2019
	        //Result: true 2019
	        System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t"+atomicInteger.get());
	        //Current status value: 2019; Expected value: 5, updated value: 2022
	        //Result: false 2019 (update failed, so false is returned)
	        System.out.println(atomicInteger.compareAndSet(5, 2222)+"\t"+atomicInteger.get());
	    }
	}

UnSafe class

UnSafe class is the core class of CAS. Because Java methods cannot directly access the bottom layer, they need to be accessed through native methods. UnSafe is equivalent to a back door. Based on this class, specific memory data can be directly operated

  • The UnSafe class is sun In misc package, its internal method operation can directly operate memory like the pointer of C, because CAS operation in Java depends on the method of UnSafe class

  • All the methods in the UnSafe class are modified native ly, that is, the methods in the UnSafe class directly call the underlying resources to execute the response task

  • public final int getAndIncrement:

    The variable ValueOffset is the offset address of the variable in memory, because UnSafe obtains data according to the memory offset address

  • Variable value and volatile are decorated to ensure the visibility between multiple threads

atomic

Atomic is an atomic class, mainly including the following

The Java development manual describes:

Basic type atomic class (AtomicInteger, AtomicBoolean, AtomicLong)

methodexplain
public final int get()Gets the current value
public final int getAndSet(int newValue)Gets the current value and sets a new value
public final int getAndIncrement()Gets the current value and increments it automatically
public final int getAndDecrement()Get the current value and subtract from it
public final int getAndAdd(int delta)Gets the current value and adds the expected value
public final int incrementAndGet( )Returns the value after adding 1
boolean compareAndSet(int expect,int update)Returns true if the entered value is equal to the expected value
  • AtomicInteger solves the unsafe problem of i + + multithreading
    The code is as follows:

    public class AtomicIntegerDemo {
        AtomicInteger atomicInteger=new AtomicInteger(0);
        public void addPlusPlus(){
            atomicInteger.incrementAndGet();
        }
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch countDownLatch=new CountDownLatch(10);
            AtomicIntegerDemo atomic=new AtomicIntegerDemo();
            // 10 threads call addPlusPlus 100 times in a loop, and the final result is 10 * 100 = 1000
            for (int i = 1; i <= 10; i++) {
                new Thread(()->{
                   try{
                       for (int j = 1; j <= 100; j++) {
                           atomic.addPlusPlus();
                       }
                   }finally {
                       countDownLatch.countDown();
                   }
                },String.valueOf(i)).start();
            }
            //(1).  If the following pause time of 3 seconds is not added, the main thread will end before i++ 1000 times
            //try { TimeUnit.SECONDS.sleep(3);  } catch (InterruptedException e) {e.printStackTrace();}
            //(2).  Use CountDownLatch to solve the problem of waiting time
            countDownLatch.await();
            System.out.println(Thread.currentThread().getName()+"\t"+"Obtained result:"+atomic.atomicInteger.get());
        }
    }
    

    CountDownLatch is a synchronization tool class, which is used to coordinate the synchronization between multiple threads or talk about the communication between threads (rather than being used as mutual exclusion).

    • CountDownLatch enables a thread to wait for other threads to complete their work before continuing execution. It is implemented using a counter. The initial value of the counter is the number of threads. When each thread completes its task, the value of the counter will be reduced by one. When the counter value is 0, it means that all threads have completed some tasks, and then the threads waiting on CountDownLatch can resume executing the next tasks.
  • AtomicBoolean can be used as an interrupt identifier to stop a thread

    //Implementation of thread interrupt mechanism
    public class AtomicBooleanDemo {
        public static void main(String[] args) {
            AtomicBoolean atomicBoolean=new AtomicBoolean(false);
    
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"\t"+"coming.....");
                while(!atomicBoolean.get()){
                    System.out.println("==========");
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"over.....");
            },"A").start();
    
            new Thread(()->{
                atomicBoolean.set(true);
            },"B").start();
        }
    }
    
  • The underlying idea of AtomicLong is CAS + spin lock, which is applicable to global computing with low concurrency. After high concurrency, the performance drops sharply. The reasons are as follows: n threads modify the value of CAS, and only one thread succeeds each time. Other N-1 fails. The failed spins continuously until it succeeds. In this case, a large number of failed spins, the cpu is hit high all at once (AtomicLong's spin will become a bottleneck)
    In the case of high concurrency, we use LoadAdder

Array type atomic class (AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray)

Array type atomic class, mainly including three AtomicIntegerArray, AtomicLongArray and AtomicReferenceArray

public class AtomicIntegerArrayDemo {
    public static void main(String[] args) {
        //(1).  Create a new AtomicIntegerArray with the same length as all elements copied from the given array.
        int[]arr2={1,2,3,4,5};
        AtomicIntegerArray array=new AtomicIntegerArray(arr2);
        //(2).  Creates a new AtomicIntegerArray of the given length, with all elements initially zero.
        //AtomicIntegerArray array=new AtomicIntegerArray(5);

        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i]);
        }
        System.out.println();
        System.out.println("=======");
        array.getAndSet(0,1111);
        System.out.println("============");
        System.out.println("Change the element at position 0 in the number to:"+array.get(0));
        System.out.println("The array position is 1. The old value at position is:"+array.get(1));
        System.out.println("Add 1 to the number at the position where the array position is 1");
        array.getAndIncrement(1);
        System.out.println("The array position is 1. The new value at position is:"+array.get(1));
    }
}

Reference type atomic class (AtomicReference, AtomicStampedReference, AtomicMarkableReference)

There are three atomic classes of reference types: AtomicReference, AtomicStampedReference, and AtomicMark ableReference

  • Use AtomicReference to implement the spin lock case

    //Spin lock
    public class AtomicReferenceThreadDemo {
        static AtomicReference<Thread>atomicReference=new AtomicReference<>();
        static Thread thread;
        public static void lock(){
            thread=Thread.currentThread();
            System.out.println(Thread.currentThread().getName()+"\t"+"coming.....");
            while(!atomicReference.compareAndSet(null,thread)){
    
            }
        }
        public static void unlock(){
            System.out.println(Thread.currentThread().getName()+"\t"+"over.....");
            atomicReference.compareAndSet(thread,null);
        }
        public static void main(String[] args) {
            new Thread(()->{
                AtomicReferenceThreadDemo.lock();
                try { TimeUnit.SECONDS.sleep(3);  } catch (InterruptedException e) {e.printStackTrace();}
                AtomicReferenceThreadDemo.unlock();
            },"A").start();
    
            new Thread(()->{
                AtomicReferenceThreadDemo.lock();
                AtomicReferenceThreadDemo.unlock();
            },"B").start();
        }
    }
    
  • AtomicStampedReference solves ABA problems
    The reference type atomic class with version number can solve the ABA problem
    The solution has been modified several times
    State stamp atomic reference

  • AtomicMarkableReference does not recommend using it to solve ABA problems
    Atomic update reference type object with flag bit
    Whether to modify (its definition is to simplify the status stamp to the bit true|false), similar to disposable chopsticks
    Status stamp (true/false) atomic reference
    It is not recommended to use it to solve ABA problems

  • Differences between AtomicStampedReference and AtomicMarkableReference

    • stamped – version number, modified once + 1
    • Markable – whether true and false have been modified

Object (AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater)

Manipulate certain fields within a non thread safe object in a thread safe manner
Can you not lock the whole object, reduce the scope of locking, and only focus on a field with long-term and sensitive changes, rather than the whole object, so as to achieve the purpose of precise locking + memory saving. That is, lock only one operation
Requirements for modifying atomic classes using object properties:

  • Updated object properties must use the public volatile modifier

  • Because the property modification types of objects are abstract classes, you must use the static method newUpdater() to create an updater every time you use it, and you need to set the classes and properties you want to update. As follows:

  • Example: we use AtomicIntegerFieldUpdater to set the int element in the Score object as an atomic object.

    /***
     1.From the AtomicIntegerFieldUpdaterDemo code, it is not difficult to find that when we update the score through AtomicIntegerFieldUpdater, we do not need to call the get() method to obtain the final int value compared with AtomicInteger!
     2.For AtomicIntegerFieldUpdaterDemo class, AtomicIntegerFieldUpdater is of static final type, that is, even if 100 objects are created, there is only one AtomicIntegerField that will not occupy the memory of the object, but AtomicInteger will create multiple AtomicInteger objects, which will occupy more memory than AtomicIntegerFieldUpdater,
     Therefore, people familiar with dubbo's source code know that dubbo has a class AtomicPositiveInteger that implements polling load balancing strategy, which uses AtomicIntegerFieldUpdater.
     */
    @SuppressWarnings("all")
    public class AtomicIntegerFieldUpdaterDemo {
        private static final int THREAD_NUM = 1000;
    
        //The fence is set to prevent the main thread from outputting self increasing variables before the loop ends, resulting in the mistaken belief that the thread is unsafe
        private static CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM);
        Score score=new Score();
        public static void main(String[] args)throws InterruptedException {
            Score score = new Score();
            for (int j = 0; j < THREAD_NUM; j++) {
                new Thread(() -> {
                    score.addTotalScore(score);
                    countDownLatch.countDown();
                }).start();
            }
            countDownLatch.await();
            System.out.println("totalScore Value of:" + score.totalScore);
        }
    }
    
    class Score {
        String username;
    
        public volatile int totalScore = 0;
        //public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass,String fieldName)
        private static AtomicIntegerFieldUpdater atomicIntegerFieldUpdater =
                AtomicIntegerFieldUpdater.newUpdater(Score.class, "totalScore");
    
        public void addTotalScore(Score score){
            //public int incrementAndGet(T obj) {
            atomicIntegerFieldUpdater.incrementAndGet(score);
        }
    }
    
    
  • AtomicReferenceFieldUpdater: updates the value of the atomic reference type field
    Example: multiple threads call the initialization method of a class concurrently. If it has not been initialized, the initialization will be executed. It is required that it can only be initialized once.

    //Requirements: multiple threads call the initialization method of a class concurrently. If it has not been initialized, the initialization will be performed. It is required that it can only be initialized once
    public class AtomicReferenceFieldUpdaterDemo {
        public static void main(String[] args) {
            MyCar myCar=new MyCar();
            AtomicReferenceFieldUpdater<MyCar,Boolean>atomicReferenceFieldUpdater=
                    AtomicReferenceFieldUpdater.newUpdater(MyCar.class,Boolean.class,"flag");
            for (int i = 1; i <= 5; i++) {
                new Thread(()->{
                    if(atomicReferenceFieldUpdater.compareAndSet(myCar,Boolean.FALSE,Boolean.TRUE)){
                        System.out.println(Thread.currentThread().getName()+"\t"+"---init.....");
                        try { TimeUnit.SECONDS.sleep(2);  } catch (InterruptedException e) {e.printStackTrace();}
                        System.out.println(Thread.currentThread().getName()+"\t"+"---init.....over");
                    }else{
                        System.out.println(Thread.currentThread().getName()+"\t"+"------Another thread is initializing");
                    }
                },String.valueOf(i)).start();
            }
    
        }
    }
    class MyCar{
        public volatile Boolean flag=false;
    }
    
    
  • Reflections on the use of AtomicIntegerFieldUpdater and AtomicInteger
    Through the following code, it is not difficult for us to know that the effect of using AtomicIntegerFieldUpdater is consistent with that of AtomicInteger. Since the God of AtomicInteger concurrency already exists, what about writing an AtomicIntegerFieldUpdater?

    • From the AtomicIntegerFieldUpdaterDemo code, it is not difficult to find that when we update the score through AtomicIntegerFieldUpdater, we do not need to call the get() method to obtain the final int value compared with AtomicInteger!
    • For the AtomicIntegerFieldUpdaterDemo class, the AtomicIntegerFieldUpdater is of static final type, that is, even if 100 objects are created, there is only one AtomicIntegerField that will not occupy the memory of the object
    • However, AtomicInteger creates multiple AtomicInteger objects, which takes up more memory than AtomicIntegerFieldUpdater
    • Therefore, people familiar with dubbo's source code know that dubbo has a class that implements polling load balancing strategy. AtomicPositiveInteger uses AtomicIntegerFieldUpdate, which is widely used at the bottom of netty
     
        public static class Candidate {
            int id;
     
            volatile int score = 0;
     
            AtomicInteger score2 = new AtomicInteger();
        }
     
        public static final AtomicIntegerFieldUpdater<Candidate> scoreUpdater =
                AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
     
        public static AtomicInteger realScore = new AtomicInteger(0);
     
        public static void main(String[] args) throws InterruptedException {
            final Candidate candidate = new Candidate();
            Thread[] t = new Thread[10000];
            for (int i = 0; i < 10000; i++) {
                t[i] = new Thread() {
                    @Override
                    public void run() {
                        if (Math.random() > 0.4) {
                            candidate.score2.incrementAndGet();
                            scoreUpdater.incrementAndGet(candidate);
                            realScore.incrementAndGet();
                        }
                    }
                };
                t[i].start();
            }
            for (int i = 0; i < 10000; i++) {
                t[i].join();
            }
            System.out.println("AtomicIntegerFieldUpdater Score=" + candidate.score);
            System.out.println("AtomicInteger Score=" + candidate.score2.get());
            System.out.println("realScore=" + realScore.get());
     
        }
    }
    /**
        AtomicIntegerFieldUpdater Score=5897
        AtomicInteger Score=5897
        realScore=5897
    */
    
    

Atomic operation enhancement classes (DoubleAccumulator, DoubleAdder, LongAccumulator, LongAdder)

methodexplain
void add(long x)Add 1 to the current value
void increment( )Add 1 to the current value
void decrement( )Subtract the current value by 1
long sum( )Returns the current value, especially if value is not updated concurrently.
long longvaleEquivalent to long sum().
long sumThenReset()Get the current value and reset value to 0
  • LongAdder and LongAccumulato
    LongAdder can only be used to calculate addition and subtraction, and starts from zero
    The LongAccumulator provides custom function operations

    public class LongAdderDemo {
        public static void main(String[] args) {
            // LongAdder can only do addition and subtraction, not multiplication and division
            LongAdder longAdder=new LongAdder();
            longAdder.increment();
            longAdder.increment();
            longAdder.increment();
            longAdder.decrement();
    
            System.out.println(longAdder.longValue());
            System.out.println("========");
            //LongAccumulator​(LongBinaryOperator accumulatorFunction, long identity)
            //LongAccumulator longAccumulator=new LongAccumulator((x,y)->x+y,0);
            LongAccumulator longAccumulator=new LongAccumulator(new LongBinaryOperator() {
                @Override
                public long applyAsLong(long left, long right) {
                    return left*right;
                }
            },5);
            longAccumulator.accumulate(1);
            longAccumulator.accumulate(2);
            longAccumulator.accumulate(3);
            System.out.println(longAccumulator.longValue());
        }
    }
    
    
  • LongAdder high performance comparison code demo

    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.concurrent.atomic.AtomicLong;
    import java.util.concurrent.atomic.LongAccumulator;
    import java.util.concurrent.atomic.LongAdder;
    
    class ClickNumber {
        int number=0;
    
        //(1).  Implementing number using synchronized++
        public synchronized void add_synchronized(){
            number++;
        }
        //(2).  Using AtomicInteger
        AtomicInteger atomicInteger=new AtomicInteger();
        public void add_atomicInteger(){
            atomicInteger.incrementAndGet();
        }
        //(3).  Using AtomicLong
        AtomicLong atomicLong=new AtomicLong();
        public void add_atomicLong(){
            atomicLong.incrementAndGet();
        }
        //(4).  Using LongAdder
        LongAdder adder=new LongAdder();
        public void add_longAdder(){
            adder.increment();
        }
        //(5).  Using the longaccumulator
        LongAccumulator accumulator=new LongAccumulator((x, y)->x+y,0);
        public void add_longAccumulater(){
            accumulator.accumulate(1);
        }
    }
    /**
     * 50 Threads, 100w times per thread, total likes
     * */
    public class LongAdderCalcDemo {
        // 50 threads and each thread point at 100w times
        public static  final int SIZE_THREAD=50;
        public static  final int _1w=10000;
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch countDownLatch=new CountDownLatch(SIZE_THREAD);
            ClickNumber clickNumber=new ClickNumber();
            long startTime = System.currentTimeMillis();
            for (int i = 1 ; i <=SIZE_THREAD ; i++) {
                new Thread(()->{
                    try{
                        for (int j = 1; j <=10*_1w; j++) {
                            //We can clearly see that calling LongAdder performs best
                            //clickNumber.add_synchronized();
                            clickNumber.add_longAdder();
                        }
                    }finally {
                        countDownLatch.countDown();
                    }
                },String.valueOf(i)).start();
            }
            countDownLatch.await();
            long endTime = System.currentTimeMillis();
            System.out.println("-----consTime:"+(endTime-startTime)+"millisecond"+"\t");
            System.out.println(clickNumber.adder.longValue());
    
        }
    
    }
    
    
  • The results show that

    // clickNumber. add_ Result of longadder()
    -----consTime:107 millisecond	
    5000000
    //  clickNumber.add_synchronized();  Results
    -----consTime:447 millisecond
    5000000	
    

Disadvantages of CAS

  • Long cycle time and high overhead
    We can see that when the getAndInt method executes, there is a do while
    If CAS fails, it will keep trying. If CAS fails for a long time, it may bring great overhead to CPU
  • The atomicity of one shared variable can be guaranteed, but the atomicity of multiple shared variables cannot be guaranteed.
    When operating on a shared variable, we can use cyclic CAS to ensure atomic operation
    When operating on multiple shared variables, cyclic CAS cannot guarantee the atomicity of the operation. At this time, locks can be used to ensure the atomicity

ABA problem

Cause
Suppose there are two threads, T1 and T2, respectively. Then T1 executes an operation for 10 seconds and T2 executes an operation for 2 seconds. At first, AB threads obtain the value of A from the main memory respectively. However, because B executes faster, he first changes the value of A to B, then modifies it to A, and then completes the execution. T1 thread completes the execution after 10 seconds, Judging that the value in the memory is A, and it is the same as its expected value, it thinks that no one has changed the value in the main memory and happily modifies it to B, but in fact, it may have experienced the transformation of ABCDEFA in the middle, that is, the middle value has experienced the transformation of civet cat for prince.

import java.util.concurrent.atomic.AtomicInteger;

public class ABADemo {
    static AtomicInteger atomicInteger = new AtomicInteger(100);

    public static void main(String[] args) {
        new Thread(() -> {
            atomicInteger.compareAndSet(100, 101);
            atomicInteger.compareAndSet(101, 100);
        }, "t1").start();

        new Thread(() -> {
            //Pause the thread for a moment
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ;
            System.out.println(atomicInteger.compareAndSet(100, 2019) + "\t" + atomicInteger.get());
        }, "t2").start();



    }
}

result:

true	2019

Therefore, the ABA problem is that when obtaining the main memory value, the memory value has been modified N times when we write it into the main memory, but it has finally been changed to the original value. Although the result is no problem, when thread B judges, atomoicInteger has actually been modified. If we use the result of B to judge whether atomoicInteger has been modified, we can't get the correct answer.

ABA problem solution

  • ①. The solution to the ABA problem is to increase the version number: each modification of the AtomicStampedReference will have a version number

  • ②. Note: AtomicStampedReference is used to solve the ABA problem in AtomicInteger. This demo attempts to increase the value of integer from 0 to 1000, but it will stop growing when the value of integer increases to 128. There are two reasons for this phenomenon:

    • Use the int type instead of Integer to save the current value
    • Interger's cache of - 128 ~ 127 [this range is valid. If it is not in this range, comareAndSet will always return false
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.concurrent.atomic.AtomicStampedReference;
    
    public class ABADemo {
        private static AtomicStampedReference<Integer> stampedReference=new AtomicStampedReference<>(100,1);
        public static void main(String[] args) {
            new Thread(()->{
                int stamp = stampedReference.getStamp();
                System.out.println(Thread.currentThread().getName()+"\t First version number"+stamp+"\t Value is"+stampedReference.getReference());
                //Pause t3 thread for 1 second
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
    
                stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
                System.out.println(Thread.currentThread().getName()+"\t Second version number"+stampedReference.getStamp()+"\t Value is"+stampedReference.getReference());
                stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
                System.out.println(Thread.currentThread().getName()+"\t 3rd version number"+stampedReference.getStamp()+"\t Value is"+stampedReference.getReference());
            },"t3").start();
    
            new Thread(()->{
                int stamp = stampedReference.getStamp();
                System.out.println(Thread.currentThread().getName()+"\t First version number"+stamp+"\t Value is"+stampedReference.getReference());
                //Ensure that thread 3 completes ABA once
                try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
                boolean result = stampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
                System.out.println(Thread.currentThread().getName()+"\t Modified successfully"+result+"\t Latest version number"+stampedReference.getStamp());
                System.out.println("Latest value\t"+stampedReference.getReference());
            },"t4").start();
    
    
        }
    }
    
    

Six locks

Optimistic lock and pessimistic lock

Pessimistic lock

  • The synchronized keyword and the Lock implementation class are pessimistic locks
  • What is a pessimistic lock? I think there must be another thread to modify the data when I use the data, so I will lock it first when I get the data to ensure that the data will not be modified by other threads=
  • It is suitable for scenarios with many write operations. Locking first can ensure that the data is correct during write operations (write operations include addition, deletion and modification), and explicit locking before operating synchronous resources

Optimistic lock
Optimistic lock thinks that no other thread will modify the data when using the data, so it will not add the lock. It just judges whether other threads have updated the data before updating the data.

  • If the data is not updated, the current thread successfully writes the modified data.
  • If the data has been updated by other threads, different operations are performed according to different implementation methods
  • Optimistic locking is implemented by using lock free programming in Java. The most commonly used is CAS algorithm. The increment operation in Java atomic class is realized by CAS spin

It is suitable for scenarios with many read operations. The feature of no lock can greatly improve the performance of read operations
Optimistic locks are generally implemented in two ways (using version number mechanism and CAS algorithm)

example:

	//Call mode of pessimistic lock
	public synchronized void m1(){
		//Business logic after locking
	}
	//Ensure that multiple threads use the same lock object
	ReetrantLock lock=new ReentrantLock();
	public void m2(){
		lock.lock();
		try{
			//Operation synchronization resource
		}finally{
			lock.unlock();
		}
	}
	//Optimistic lock calling method
	//Ensure that multiple threads use the same AtomicInteger
	private  AtomicInteger atomicIntege=new AtomicInteger();
	atomicIntege.incrementAndGet();

Fair lock and unfair lock

concept

  • Fair lock: it means that multiple threads acquire locks in the order of applying for locks, similar to queuing for rice first come first served

  • Unfair lock: it means that the order in which multiple threads acquire locks is not the order in which they apply for locks. It is possible that the thread that applies later obtains locks first than the thread that applies first. In the case of high concurrency, it may cause priority reversal or starvation
    Note: synchronized and ReentrantLock are non fair locks by default

  • Example: case of queuing for tickets (fair lock hunger)
    Lock starvation: we use five threads to buy 100 tickets. The ReentrantLock defaults to an unfair lock. The results obtained may be that thread A is selling these 100 tickets, which will lead to lock starvation in threads B, C, D and E

    class Ticket {
        private int number = 50;
    
        private Lock lock = new ReentrantLock(true); //By default, the non fair lock is used. The average distribution is a little bit, and = -- -- "fair" is a little bit
        public void sale() {
            lock.lock();
            try {
                if(number > 0) {
                    System.out.println(Thread.currentThread().getName()+"\t Sell No: "+(number--)+"\t Remaining: "+number);
                }
            }finally {
                lock.unlock();
            }
        }
        /*Object objectLock = new Object();
    
        public void sale(){
            synchronized (objectLock)
            {
                if(number > 0)
                {
                    System.out.println(Thread.currentThread().getName()+"\t Sold number: "+ (number -- +" \ tthe remaining: "+ number);
                }
            }
        }*/
    }
    public class SaleTicketDemo {
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
            new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"a").start();
            new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"b").start();
            new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"c").start();
            new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"d").start();
            new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"e").start();
        }
    }
    
    

Source code analysis

  • Fair lock: sort queue fair lock is to judge whether there are pioneer nodes in the synchronization queue. If there is no pioneer node, it can be locked
  • The first to occupy, first to get, unfair lock doesn't matter. It can run as long as it can grab the synchronization state
    ReentrantLock is a non fair lock by default. A fair lock requires one more method, so the performance of a non fair lock is better (aqs source code)

Why are there fair locks and unfair locks? Why is it unfair by default

  • There is still a time difference between restoring the suspended thread and obtaining the real lock. From the perspective of developers, this time is very small, but from the perspective of CPU, this time is still obvious. Therefore, unfair lock can make full use of the CPU time slice and minimize the CPU idle state time
  • When using multithreading, an important consideration is the overhead of thread switching. When using an unfair lock, when a thread requests the lock to obtain the synchronization state and then releases the synchronization state, because it does not need to consider whether there is a precursor node, the probability that the thread that just released the lock will obtain the synchronization state again becomes very large, so the overhead of the thread is reduced

When to use fair? When to use unfair?

For higher throughput, it is obvious that unfair locking is more appropriate, because it saves a lot of thread switching time, and the throughput naturally goes up. Otherwise, use the fair lock and everyone will use it fairly

Reentrant lock (also known as recursive lock)

concept
Reentrant lock:

  • Yes: Yes
  • Heavy: again
  • Enter: enter, enter what: enter the synchronization domain (i.e. synchronization code block, method or code showing lock locking)
  • Locks: synchronous locks

Reentrant lock, also known as recursive lock, means that when the same thread acquires a lock in the outer method, the inner method of the thread will automatically acquire the lock (provided that the lock object must be the same object) and will not be blocked because it has been acquired before and has not been released

  • If it is a recursive call method decorated with synchronized, and the second entry of the program is blocked by itself, isn't it a big joke and self binding. Therefore, ReentrantLock and synchronized in Java are reentrant locks. One advantage of reentrant locks is that deadlocks can be avoided to a certain extent

The code verifies that synchronized is a reentrant lock

//synchronized is a reentrant lock
class Phone{
    public synchronized void sendSms() throws Exception{
        System.out.println(Thread.currentThread().getName()+"\tsendSms");
        sendEmail();
    }
    public synchronized void sendEmail() throws Exception{
        System.out.println(Thread.currentThread().getName()+"\tsendEmail");
    }

}
/**
 * Description:
 *  Reentrant locks (also called recursive locks)
 *  It refers to the code that the inner hostile function can still obtain the lock after the same outer function obtains the lock
 *  When the outer method of the same thread acquires a lock, it will automatically acquire the lock when entering the inner method
 *  *  That is, a thread can enter any block of code synchronized with the lock it has marked
 *  **/
public class ReenterLockDemo {
    /**
     * t1 sendSms
     * t1 sendEmail
     * t2 sendSms
     * t2 sendEmail
     * @param args
     */
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.sendSms();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"t1").start();
        new Thread(()->{
            try {
                phone.sendSms();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"t2").start();
    }
}

ReentrantLock is a reentrant lock

//ReentrantLock is a reentrant lock
	class Phone implements Runnable {
	    private Lock lock = new ReentrantLock();
	
	    @Override
	    public void run() {
	        get();
	    }
	
	    private void get() {
	        lock.lock();
	        try {
	            System.out.println(Thread.currentThread().getName() + "\tget");
	            set();
	        } finally {
	            lock.unlock();
	        }
	    }
	
	    private void set() {
	        lock.lock();
	        try {
	            System.out.println(Thread.currentThread().getName() + "\tset");
	        } finally {
	            lock.unlock();
	        }
	    }
	}
	
	/**
	 * Description:
	 * Reentrant locks (also called recursive locks)
	 * It refers to the code that the inner hostile function can still obtain the lock after the same outer function obtains the lock
	 * When the outer method of the same thread acquires a lock, it will automatically acquire the lock when entering the inner method
	 * <p>
	 * That is, a thread can enter any block of code synchronized with the lock it has marked
	 **/
	public class ReenterLockDemo {
	    /**
	     * Thread-0 get
	     * Thread-0 set
	     * Thread-1 get
	     * Thread-1 set
	     */
	    public static void main(String[] args) {
	        Phone phone = new Phone();
	        Thread t3 = new Thread(phone);
	        Thread t4 = new Thread(phone);
	        t3.start();
	        t4.start();
	
	    }
	}

Types of reentrant locks

  • The implicit lock (that is, the lock used by the synchronized keyword) is a reentrant lock by default, which is used in the synchronization block and synchronization method
    When you call other synchronized modified methods or code blocks of this class inside a synchronized modified method or code block, you can always get a lock
  • Display locks (i.e. locks) also have reentrant locks such as ReentrantLock
    lock and unlock must match one by one. If there is less or more, it will pit other threads

Implementation mechanism of Synchronized reentry
Implementation mechanism of Synchronized reentry (why can any object become a lock)

  • Each lock object has a lock counter and a pointer to the thread holding the lock
  • When the monitorenter is executed, if the counter of the target lock object is zero, it indicates that it is not held by other threads. The Java virtual opportunity sets the holding thread of the lock object as the current thread and increases the counter by 1
  • When the counter of the target lock object is not zero, if the holding thread of the lock object is the current thread, the Java virtual machine can increase its counter by 1, otherwise it needs to wait until the holding thread releases the lock. The number of this counter can also be called the number of resulting reentries.
  • When monitorexit is executed, the Java virtual machine needs to decrease the counter of the lock object by 1. A zero counter indicates that the lock has been released

Deadlock and troubleshooting

Deadlock refers to the phenomenon that two or more threads wait for each other due to competing for resources during execution. Without external interference, they will not be able to move forward. If resources are sufficient and the resource requests of the process can be met, the possibility of deadlock is very low, otherwise they will fall into deadlock due to competing for limited resources

Causes of deadlock

  • Insufficient system resources

  • The sequence of process running and advancing is inappropriate

  • Improper allocation of resources

  • Examples are as follows:

    public class DeadLockDemo{
    
        static Object lockA = new Object();
        static Object lockB = new Object();
        public static void main(String[] args){
            Thread a = new Thread(() -> {
                synchronized (lockA) {
                    System.out.println(Thread.currentThread().getName() + "\t" + " Own holding A Lock, expect to get B lock");
    
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    synchronized (lockB) {
                        System.out.println(Thread.currentThread().getName() + "\t get B Lock successful");
                    }
                }
            }, "a");
            a.start();
    
            new Thread(() -> {
                synchronized (lockB){
                
                    System.out.println(Thread.currentThread().getName()+"\t"+" Own holding B Lock, expect to get A lock");
    
                    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
    
                    synchronized (lockA){
                    
                        System.out.println(Thread.currentThread().getName()+"\t get A Lock successful");
                    }
                }
            },"b").start();
    
    
        }
    }
    
    
  • Deadlock elimination scheme 1: jstack process number
    As follows:

    D:\studySoft\Idea201903\JavaSelfStudy>jps
    10048 Launcher
    6276 DeadLockDemo
    6332 Jps
    9356
    D:\studySoft\Idea201903\JavaSelfStudy>jstack 6276 (The last one found a deadlock)
    2021-07-28 16:05:36
    Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.111-b14 mixed mode):
    
    "DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x0000000003592800 nid=0x830 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "b" #15 prio=5 os_prio=0 tid=0x00000000253d5000 nid=0x1ba8 waiting for monitor entry [0x0000000025c8e000]
       java.lang.Thread.State: BLOCKED (on object monitor)
            at com.xiaozhi.juc.DeadLockDemo.lambda$main$1(DeadLockDemo.java:31)
            - waiting to lock <0x0000000741404050> (a java.lang.Object)
            - locked <0x0000000741404060> (a java.lang.Object)
            at com.xiaozhi.juc.DeadLockDemo$$Lambda$2/2101440631.run(Unknown Source)
            at java.lang.Thread.run(Thread.java:745)
    
    "a" #14 prio=5 os_prio=0 tid=0x00000000253d3800 nid=0xad8 waiting for monitor entry [0x0000000025b8e000]
       java.lang.Thread.State: BLOCKED (on object monitor)
            at com.xiaozhi.juc.DeadLockDemo.lambda$main$0(DeadLockDemo.java:20)
            - waiting to lock <0x0000000741404060> (a java.lang.Object)
            - locked <0x0000000741404050> (a java.lang.Object)
            at com.xiaozhi.juc.DeadLockDemo$$Lambda$1/1537358694.run(Unknown Source)
            at java.lang.Thread.run(Thread.java:745)
    
    "Service Thread" #13 daemon prio=9 os_prio=0 tid=0x000000002357b800 nid=0x1630 runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "C1 CompilerThread3" #12 daemon prio=9 os_prio=2 tid=0x00000000234f6000 nid=0x1fd4 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "C2 CompilerThread2" #11 daemon prio=9 os_prio=2 tid=0x00000000234f3000 nid=0x5c0 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "C2 CompilerThread1" #10 daemon prio=9 os_prio=2 tid=0x00000000234ed800 nid=0x1afc waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "C2 CompilerThread0" #9 daemon prio=9 os_prio=2 tid=0x00000000234eb800 nid=0x2ae0 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "JDWP Command Reader" #8 daemon prio=10 os_prio=0 tid=0x0000000023464800 nid=0xc50 runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "JDWP Event Helper Thread" #7 daemon prio=10 os_prio=0 tid=0x000000002345f800 nid=0x1b0c runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "JDWP Transport Listener: dt_socket" #6 daemon prio=10 os_prio=0 tid=0x0000000023451000 nid=0x2028 runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000002343f800 nid=0x1ea0 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x00000000233eb800 nid=0x10dc runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Finalizer" #3 daemon prio=8 os_prio=1 tid=0x00000000233d3000 nid=0xafc in Object.wait() [0x000000002472f000]
       java.lang.Thread.State: WAITING (on object monitor)
            at java.lang.Object.wait(Native Method)
            - waiting on <0x0000000741008e98> (a java.lang.ref.ReferenceQueue$Lock)
            at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
            - locked <0x0000000741008e98> (a java.lang.ref.ReferenceQueue$Lock)
            at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
            at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
    
    "Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000021d0d000 nid=0x28ec in Object.wait() [0x000000002462f000]
       java.lang.Thread.State: WAITING (on object monitor)
            at java.lang.Object.wait(Native Method)
            - waiting on <0x0000000741006b40> (a java.lang.ref.Reference$Lock)
            at java.lang.Object.wait(Object.java:502)
            at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
            - locked <0x0000000741006b40> (a java.lang.ref.Reference$Lock)
            at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
    
    JNI global references: 2504
    
    
    Found one Java-level deadlock:
    =============================
    "b":
      waiting to lock monitor 0x0000000021d10b58 (object 0x0000000741404050, a java.lang.Object),
      which is held by "a"
    "a":
      waiting to lock monitor 0x0000000021d13498 (object 0x0000000741404060, a java.lang.Object),
      which is held by "b"
    
    Java stack information for the threads listed above:
    ===================================================
    "b":
            at com.xiaozhi.juc.DeadLockDemo.lambda$main$1(DeadLockDemo.java:31)
            - waiting to lock <0x0000000741404050> (a java.lang.Object)
            - locked <0x0000000741404060> (a java.lang.Object)
            at com.xiaozhi.juc.DeadLockDemo$$Lambda$2/2101440631.run(Unknown Source)
            at java.lang.Thread.run(Thread.java:745)
    "a":
            at com.xiaozhi.juc.DeadLockDemo.lambda$main$0(DeadLockDemo.java:20)
            - waiting to lock <0x0000000741404060> (a java.lang.Object)
            - locked <0x0000000741404050> (a java.lang.Object)
            at com.xiaozhi.juc.DeadLockDemo$$Lambda$1/1537358694.run(Unknown Source)
            at java.lang.Thread.run(Thread.java:745)
    
    Found 1 deadlock.
    

    As you can see, we found a deadlock through jstack 6276: "b" and "a"

  • Deadlock elimination scheme 2: jconsole
    jconsole is a built-in analysis tool for java:


Spin lock

Spin lock means that the thread trying to obtain the lock will not block immediately, but will try to obtain the lock in a circular way.

  • When the thread finds that the lock is occupied, it will continuously cycle to judge the state of the lock until it is obtained. This has the advantage of reducing the consumption of thread context switching, but the disadvantage is that the loop will consume CPU
//Spin lock
public class AtomicReferenceThreadDemo {
    static AtomicReference<Thread>atomicReference=new AtomicReference<>();
    static Thread thread;
    public static void lock(){
        thread=Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t"+"coming.....");
        while(!atomicReference.compareAndSet(null,thread)){

        }
    }
    public static void unlock(){
        System.out.println(Thread.currentThread().getName()+"\t"+"over.....");
        atomicReference.compareAndSet(thread,null);
    }
    public static void main(String[] args) {
        new Thread(()->{
            AtomicReferenceThreadDemo.lock();
            try { TimeUnit.SECONDS.sleep(3);  } catch (InterruptedException e) {e.printStackTrace();}
            AtomicReferenceThreadDemo.unlock();
        },"A").start();

        new Thread(()->{
            AtomicReferenceThreadDemo.lock();
            AtomicReferenceThreadDemo.unlock();
        },"B").start();
    }
}

Topics: Concurrent Programming