Thread concurrency, how to context switch

Posted by amir on Thu, 09 Dec 2021 17:55:42 +0100

1. What is thread concurrency?

Concurrency: a cpu kernel can only execute one thread at a time and switch back and forth between multiple threads. Because the cpu switching speed is very fast, the effect of running at the same time is concurrency (actually not at the same time).

If switching back and forth?

Program counter: when each thread is created, it will allocate independent memory (stack and program counter). The program counter points to the location of bytecode executed by the current thread, so as to switch back and forth between threads.

What if the important instructions of the current thread can be executed without being switched?

For example: simulate bank transfer, transfer between ten accounts at random, and output the general ledger for each transfer. The general ledger will not change.

import java.util.Arrays;
import java.util.Random;

public class BankDemo {
    //Customer array
     int[] customers = new int[100];
    //Initialize account
    {
        Arrays.fill(customers,10000);
    }
    //Transfer method
    public   void transfer(int a,int b,int money){
        if(customers[a]<money){
            new RuntimeException("Sorry, your credit is running low");
        }
        customers[a]-=money;
        System.out.println(a+"Transfer to"+b+": "+money);
        customers[b]+=money;
        System.out.println(b+"received"+a+": "+money+"General ledger:"+total());
    }
    //Statistical general ledger method
    public int total(){
        int sum=0;
        for (int i = 0; i < customers.length; i++) {
            sum+=customers[i];
        }
        return sum;
    }

    public static void main(String[] args) {
        Random random = new Random();
        BankDemo bankDemo = new BankDemo();
        for (int i = 0; i <10 ; i++) {
            new Thread(()->{
                int money = random.nextInt(10000);
                int a = random.nextInt(100);
                int b = random.nextInt(100);
                bankDemo.transfer(a,b,money);
            }).start();
        }
    }
}

After running, it will be found that the general ledger in the middle will deviate.

What are the reasons for this?

This is because the instructions in one thread of the cpu have not been executed yet (money has not been deducted from one account and money has not been added to the other account), which are preempted by other threads, resulting in data deviation. This raises thread safety issues.

2. Thread safety

The premise of thread safety problems

At the same time, multiple threads execute the same instruction or the same variable.

What if you solve the thread safety problem?

Lock the program, let a thread execute all instructions, release the lock, and then execute other threads.  

3. Several ways of locking

Locks are divided into optimistic locks and pessimistic locks.

Pessimistic lock: think threads are unsafe and all programs are locked

1. Synchronization method

Add the synchronized keyword to the method

//Transfer method (synchronization method)
    public synchronized   void transfer(int a,int b,int money){
        if(customers[a]<money){
            new RuntimeException("Sorry, your credit is running low");
        }
        customers[a]-=money;
        System.out.println(a+"Transfer to"+b+": "+money);
        customers[b]+=money;
        System.out.println(b+"received"+a+": "+money+"General ledger:"+total());
    }

2. Synchronization code block

Lock a piece of code to be executed

synchronized(this){  //(sync code block)
            customers[a]-=money;
            System.out.println(a+"Transfer to"+b+": "+money);
            customers[b]+=money;
            System.out.println(b+"received"+a+": "+money+"General ledger:"+total());
        }

this represents the lock object

Non static methods use this, and static methods use class names class

3. Synchronous lock

In Java Lock interface in concurrent package

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

public class BankDemo3 {
    //Customer array
     int[] customers = new int[100];
    Lock lock = new ReentrantLock();
    //Initialize account
    {
        Arrays.fill(customers,10000);
    }
    //Transfer method
    public  void transfer(int a,int b,int money){
        if(customers[a]<money){
            new RuntimeException("Sorry, your credit is running low");
        }
        lock.lock();
        try{
            customers[a]-=money;
            System.out.println(a+"Transfer to"+b+": "+money);
            customers[b]+=money;
            System.out.println(b+"received"+a+": "+money+"General ledger:"+total());
        }finally {
            lock.unlock();
        }

    }
    //Statistical general ledger method
    public int total(){
        int sum=0;
        for (int i = 0; i < customers.length; i++) {
            sum+=customers[i];
        }
        return sum;
    }

    public static void main(String[] args) {
        Random random = new Random();
        BankDemo3 bankDemo = new BankDemo3();
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                int money = random.nextInt(10000);
                int a = random.nextInt(100);
                int b = random.nextInt(100);
                bankDemo.transfer(a,b,money);
            }).start();
        }
    }
}

Basic method:

Lock lock

unlock

If the unlocking is not executed, if a thread is not unlocked after it ends, other threads cannot execute and the program cannot end. So write it in finall.

Common implementation classes

  • ReentrantLock reentrant lock (as shown above)

  • WriteLock write lock

  • ReadLock read lock

  • ReadWriteLock read write lock

Comparison of the above three methods:

Granularity (range of thread locks)

Synchronization method > synchronization code block > synchronization lock

Execution efficiency

Synchronization method < synchronization code block < synchronization lock

Writing complexity

Synchronization method < synchronization code block / synchronization lock

Optimistic lock: think that thread safety problems are not common and will not lock the code

Implementation method:

1. 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.

2. CAS (Compare And Swap) comparison and exchange algorithm

Obtain the data value through the memory offset, calculate an expected value, and compare the submitted actual value with the expected value. If it is the same, modify it, and if it is different, do not modify it

Comparison between optimistic lock and pessimistic lock

Optimistic lock: more lightweight, less resource consumption, higher efficiency, suitable for more reading and less writing;

Pessimistic lock: more heavyweight, more resources consumed, slow efficiency, suitable for less reading and more writing;

4. Implementation case: for loop operation count++

Atomic class:

AtomicInteger class

Atomic integers, the bottom layer uses CAS algorithm to realize integer increment and decrement operations, so that count + + can be executed as a whole.

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    static int current = 0;
    static AtomicInteger atomic = new AtomicInteger(0);

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

  • decrementAndGet atomic decrement

ABA problem in CAS

The fundamental reason is that cas cannot record the state of the variable when modifying the variable, such as the number of modifications, and whether the variable has been modified. In this way, when one thread changes A to B, another thread will change B to A, causing the problem of multiple execution of casd.

If the expected value is inconsistent with the actual value, it will wait all the time, which will consume a lot of cpu.

ABA problem case: when the hotel does activities, it will recharge 20 yuan for customers with a membership card of less than 19 yuan, and no recharge for customers with a membership card of more than 19 yuan. If there are 100 customers and ten threads are created to execute, the final result may find that the recharge value is greater than 100 customers.

Cause of the problem: after thread A performs recharge, the customer consumes less than 19 yuan, and thread B will recharge again.

Solution: AtomicStampedReference

import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampReferenceDemo {
    static AtomicStampedReference<Integer> atomic = new AtomicStampedReference<>(19, 0);

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            int stamp = atomic.getStamp();
            System.out.println("Tag value stamp: " + stamp);
            //Recharge thread
            new Thread(() -> {
                while (true) {
                    Integer count = atomic.getReference();
                    if (count < 20) {
//                Four parameters represent the meaning: expected value, new value written, expected tag, and new tag value
                        if (atomic.compareAndSet(count, count + 20, stamp, stamp + 1)) {
                            System.out.println("RMB 20 is recharged after meeting the conditions, and the balance is:" + atomic.getReference());
                            break;
                        }
                    } else {
                        System.out.println("Does not meet recharge conditions");
                        break;
                    }
                }
            }).start();
        }


        //Consumption thread
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                while (true) {
                    int stamp = atomic.getStamp();
                    System.out.println("The tag value is:" + stamp);
                    Integer money = atomic.getReference();
                    if (money > 10) {
                        if (atomic.compareAndSet(money, money - 10, stamp, stamp + 1)) {
                            System.out.println("Successfully consumed 10 yuan, and the balance is:" + atomic.getReference());
                            break;
                        }
                    } else {
                        System.out.println("Sorry, your credit is running low");
                        break;
                    }
                }
            }
        }).start();
    }
}

ThreadLocal class

For thread local variables, a variable copy will be allocated to each thread, and the variables in the thread do not affect each other.

public class ThreadLocalDemo {
    //Thread local variable
   static  ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
       @Override
       protected Integer initialValue() {
           return 0;
       }
   };

    public static void main(String[] args) {
        for (int i = 0; i <10 ; i++) {
            new Thread(()->{
                local.set(local.get()+1);
                System.out.println(Thread.currentThread().getName()+":"+local.get());
            }).start();
        }
    }
}

Comparison between AtomicInteger class and ThreadLocal class:

AtomicInteger: make the instructions as a whole. For example, multiple people play football and each person plays two feet (the two instructions are executed together).

ThreadLocal: assign a thread copy to each thread, and each person sends a football (each football does not affect each other)

5. Case:

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)

Analyze the cause of the problem and solve the problem

Problem: different objects will appear, and singleton mode will only have one instance.

Cause: when a thread calls a method to create an object, it is preempted by another thread, resulting in the creation of different objects.

Solution: lock the program (synchronous lock)

Singleton mode

public class SunDemo {

    private static SunDemo sunDemo=null;

    private SunDemo() {

    }

    public static SunDemo getSun(){
        if(sunDemo==null){
            sunDemo=new SunDemo();
        }
        return sunDemo;
    }
}

create object

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

public class SunThread {
    static Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        for (int i = 0; i <100 ; i++) {
            new Thread(()->{
                lock.lock();
                try{
                    SunDemo sun = SunDemo.getSun();
                    System.out.println("Got the object,"+sun);
                }finally {
                    lock.unlock();
                }
            }).start();
        }
    }
}

result:

Summary

The premise and solution of thread safety problems. Optimistic lock: synchronization method, synchronization code block, synchronization lock.

Pessimistic lock: CAS, atomic class. ABA problem in CAS and its solution: AtomicStampedReference.

catalogue

1. What is thread concurrency?

If switching back and forth?

What if the important instructions of the current thread can be executed without being switched?

What are the reasons for this?

2. Thread safety

The premise of thread safety problems

What if you solve the thread safety problem?

3. Several ways of locking

Pessimistic lock: think threads are unsafe and all programs are locked

Comparison of the above three methods:

Optimistic lock: think that thread safety problems are not common and will not lock the code

Comparison between optimistic lock and pessimistic lock

4. Implementation case: for loop operation count++

Atomic class:

AtomicInteger class

ABA problem in CAS

Solution: AtomicStampedReference

ThreadLocal class

Comparison between AtomicInteger class and ThreadLocal class:

5. Case:

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)

Analyze the cause of the problem and solve the problem

Summary

Topics: Java Back-end