Thread Foundation (optimistic lock and pessimistic lock)

Pessimistic lock

When modifying a piece of data, in order to prevent others from changing the data at the same time, we can lock the data through the locking mechanism to prevent concurrency problems;  

Threads think that thread security problems are easy to occur and lock the code.

Because the process of locking and releasing pessimistic locks consumes a lot of resources, it will reduce the performance of the program.

(see the previous chapter for the locking mechanism)

Optimistic locking

Compared with pessimistic locks, optimistic locks believe that thread problems are not easy to occur, so they will not lock the code.

Optimistic lock itself does not lock the data of operations, but is implemented through some business operations. If there are too many threads, it is not recommended to use it;

Implementation method:

  • Version number mechanism

    Use the version number to record the number of data updates. Once the version number is updated plus 1, the thread will judge whether the version number is the number of times it updates itself after modifying the data. If not, the data will not be updated.

  • CAS (Compare And Swap) comparison and exchange algorithm

    • Get the value of the data by the offset of the memory

    • Calculate an expected value

    • Compare the submitted actual value with the expected value. If it is the same, modify it. If it is different, do not modify it

Comparison between pessimistic lock and optimistic lock

  • Pessimistic locks are more heavyweight, occupy more resources, and the application thread competition is more frequent, which is a scenario of writing more and reading less

  • Optimistic lock is more lightweight and has higher performance. It is used in scenarios where there is less thread competition and more reads and less writes

public class AtomicDemo {

    static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
            new Thread(() ->{
                count++;
            }).start();
        }
        System.out.println(count);
    }
}

Problem: multiple threads execute + + operations at the same time, and the final results are less

analysis:

count + + is decomposed into three instructions:

  1. Read the value of count from memory

  2. Calculate the value of count+1

  3. Assign the calculation result to count

These three instructions are not atomic. Thread A reads the count value of 10, adds 1 to get 11, and is ready to assign it to count; Thread B reads that the count is also 10, adds 1 to get 11, and assigns A value to count of 11; Switch thread A and assign count to 11.

Solution:

  1. Pessimistic lock, using synchronization method, synchronization block and synchronization lock

  2. Optimistic lock

    Use atomic integers

Pessimistic lock solution:

public class Demo {
    static Integer count=0;
    static Object object=new Object();
    Lock lock=new ReentrantLock();
    public void aa(){
        //Synchronous lock
        lock.lock();
        try{
            count++;
        }finally {
            lock.unlock();
        }
    }
//    Synchronization method
    public synchronized void cc(){
        count++;
    }
//    Synchronous code block
    public void dd(){
        synchronized (object){
            count++;
        }
    }
    public static void main(String[] args) {
        Demo demo = new Demo();
        for (int i = 0; i < 100000; i++) {
            new Thread(()->{
                demo.dd();
            }).start();
        }
        System.out.println("count:"+count);
        
    }
}

Optimistic lock solution:

Atomic class

java. util. concurrent. Most classes under atomic package are implemented by CAS operation;

For example, AtomicInteger class:

Common methods:

  • incrementAndGet atomic increment

  • decrementAndGet atomic decrement

public class AtomicDemo {

    static AtomicInteger atomicInteger=new AtomicInteger(0);

    //Optimistic lock
    public void bb(){
        count++;
        atomicInteger.incrementAndGet();
        }
    public static void main(String[] args) {
        AtomicDemo atomicDemo = new AtomicDemo();
        for (int i = 0; i < 100000; i++) {
            new Thread(()->{
                atomicDemo.bb();
            }).start();
        }
        System.out.println("count:"+count);
        System.out.println("atomic:"+atomicInteger.get());
    }
}

Problems of CAS algorithm

1.ABA question:

count=3; Thread1 get count = 3

Thread2 gets count=3 and then modifies it to count=4; Then count=4 = > count=3;

Thread1 operates again and executes successfully, but this does not mean that there is no problem with the process;

             

2. If the expected value is inconsistent with the actual value, it is in the cycle waiting state, which consumes a lot of CPU

Write the lazy singleton mode, create 100 threads, and each thread obtains a singleton object to see if there is a problem (print the hashCode of the object to see if it is the same)

public class SingletonDemo {
    private static SingletonDemo singleton = null;

    private SingletonDemo() {
        System.out.println("create object");
    }

    public static SingletonDemo getInstance(){
        if(singleton==null){
            singleton = new SingletonDemo();
        }
        return singleton;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"-----"+SingletonDemo.getInstance().hashCode());
            }).start();
        }
    }
}

We will find:

In the case of multithreading, if the constructor is called more than once and multiple objects are generated, it is not a single instance (if this does not happen, it is because the first creation process is too fast. If this thread sleep s, it will be found)

This is because another thread is executed during the object creation process for the first time. Other threads also judge and create the object

if(singleton==null){
    singleton = new SingletonDemo();
}

solve the problem:

It can be solved by synchronization method, synchronization code block and synchronization lock

public synchronized static SingletonDemo getInstance(){
        if(singleton==null){
            singleton = new SingletonDemo();
        }
        return singleton;
    }

But this approach raises a problem:

A large number of locks and release locks are required during each execution; We know that locking and releasing a large number of locks will consume resources, and the singleton mode uses an object, so this method is obviously not the best solution.

Then the method of our singleton class is written as follows:

public synchronized static SingletonDemo getInstance(){
        //If it is empty, execute the code again to improve the performance
        if(singleton==null){
            //Synchronous code blocks guarantee atomicity
            synchronized (SingletonDemo.class){
                //create object
                if(singleton==null){
                    singleton = new SingletonDemo();
                }
            }
        }

 

Keywords: Java JavaEE security Singleton pattern

Added by php_jord on Sat, 11 Dec 2021 10:04:25 +0200