Thread foundation and three major characteristics of concurrent programming

Table of contents

1. Processes and threads

1. Thread creation method

 2. Thread state

3. End thread

2. Concurrency becomes three major features

visibility

orderliness

atomicity

synchronized

CAS optimistic lock


1. Processes and threads

        First, clarify two basic concepts, what are processes and threads:

        Process: A process is the basic unit for resource allocation by the operating system, and a running project is a process.

        Thread: Thread is the basic unit of scheduling execution, and a process is generally composed of multiple threads.

        One of the goals of concurrent programming is to fully utilize the high performance provided by multi-core CPUs and improve the response time of the interface. So is it necessary for a single-core CPU to perform multi-threaded development? The answer is definitely necessary. For example, there are two threads in a method now, one thread is dependent on user input, and the other thread has no dependence on this step, so when waiting for user input, the other thread can be executed first.

        It should be noted that the number of threads is not as high as possible. It needs to be determined according to the relevant conditions of the CPU and the project. The calculation formula is as follows, but it is only a reference quantity. Project monitoring and experience should be carried out to determine the number of threads. :

        Number of threads = number of CPU cores * CPU utilization * (1+ ratio of waiting time to computing time)

1. Thread creation method

If the thread creation method is subdivided, there are the following five types:

/**
     * 第一种,继承Thread类,重写run方法
     */
    static class MyThread extends Thread{
        @Override
        public void run(){
            System.out.println("create thread method one");
        }
    }
    /**
     * 第二种,实现Runnable接口,重写run方法,没有返回值
     */
    static class MyRun implements Runnable{
        @Override
        public void run() {
            System.out.println("create thread method TWO");
        }
    }
    /**
     * 第三种,实现Callable接口,重写call方法,有返回值
     */
    static class MyCall implements Callable<String>{
        @Override
        public String call() throws Exception {
            System.out.println("create thread method THREE");
            return "success";
        }
    }

    public static void main(String[] args) {
        // 调用线程start()方法,才是将线程启动,
        // 如果是调用run方法,那么相当于是调用一个类的普通方法
        new MyThread().start();
        new Thread(new MyRun()).start();
        // 第四种,使用lambda表达式,这种方式相当于是方法一的简写方式
        new Thread(() ->{
            System.out.println("create thread method FOUR");
        }).start();
        new Thread(new FutureTask<String>(new MyCall())).start();
        // 第五种,使用线程池创建
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(() -> {
            System.out.println("create thread method FIVE");
        });
        service.shutdown();
    }

It should be noted that after the thread is created, to start the thread is to call the current thread start() method instead of calling the run() method. Calling the run() method is just to defend against the call of ordinary methods.

 2. Thread state

        Thread states are mainly divided into the following six types:

        NEW: The thread has just been created, but the start() method has not been called to start;

        RUNNABLE: Runnable state, which can be subdivided into two states: READY and RUNNING. The difference between the two is whether the time slice is allocated by the CPU to run. If allocated, it is RUNNING state, otherwise it is READY state;

        WAITING: Waiting to be awakened, such as when calling wait(), join() and other methods, enter this state;

        TIMED WAITING: Automatically wake up after waiting for a period of time, for example, when calling the sleep(2) method, enter this state;

        BLOCKED: Blocked state, when multiple threads compete for a lock, they will be in this state while waiting for the lock to be released;

        TERMINATED: The thread is terminated and destroyed.

        The change of thread state is as follows:

        

3. End thread

         In addition to the completion of the thread running and the normal end of the thread, the following four ways can also be used to end:

         1. Call the stop() method of the thread, but this method is not recommended. It will affect the transaction and cause data inconsistency. For example, in a transaction, the entire transaction has not been executed when the stop() method is called Completed, then the data modification made before stop() will not be rolled back.

        2. suspend() to pause/resume() to resume, this pair of instructions is not recommended, because the suspend() method will not release the lock, and will keep the current thread holding it. If the resume() method has not been called, the lock Resources will never be freed.

        3. Use the volatile keyword to modify a status code. When the status code is not specified, the thread executes the relevant business program, but when the thread reaches the specified status code, the thread ends normally. But the possible problem is that if the current thread executes the wait() method, it will be in a waiting state. At this time, it cannot enter the next loop and cannot jump out of the loop and end the thread.

        4. Use interrupt to end the thread by interrupting the flag bit. This method is a method that comes with the thread, and it is better than the method of modifying the status code with the volatile keyword. The following is some introduction to interrupt.

        interrupt(): Interrupt the thread, you need to pay attention to the "interrupt" here, it just adds an interrupt flag to the current thread, it does not really interrupt the thread;

        isInterrupt(): Get the interrupt flag of the current thread

        static interrupted: Query the interrupted flag of the current thread and reset it.

        One thing to note is that the current thread is executing the wait() and sleep() methods. At this time, the current thread executes the interrupt() method, and InterruptException will be thrown. The advantage of interrupt() mentioned above is that the exception is caught by try catch, and the thread is ended in the catch, so that the thread will not be kept waiting.

2. Concurrency becomes three major features

visibility

        The so-called visibility refers to a variable shared by multiple threads. When one of the threads changes the variable, other threads can read the changed value.

        In java, the keyword volatile can be used to realize the visibility of variables, which can ensure that the data information in the working memory and the main memory are consistent. However, it should be noted that when the volatile keyword is used to modify the reference type, it can only guarantee the visibility of the entire reference type, that is, the visibility of the reference address, but it cannot guarantee the visibility of the attributes of the fields in the reference type For example, volatile T t = new T(), there is int a in T; this attribute, if the reference address of t changes, then other threads are visible, but if the value of the field a changes, then other threads is not necessarily visible.

        Analyzing visibility from the cpu level refers to the visibility of cache and memory data between different cpus. The CPU has a three-level cache. Each core in the cpu has its own first-level cache and second-level cache. Each cpu has its own third-level cache. Visibility in the cpu is achieved through a cache coherence protocol (commonly mesi protocol). It should be noted that the visibility of volatile has nothing to do with the cache coherence protocol. Combine with the following figure for further understanding

        

 ( Cache line: A cache line refers to the basic unit read from memory into the cache, with a size of 64 bytes. The annotation contended can ensure that a field less than 64 bytes in length is read as a cache line, but when using this annotation Need to add -XX:-RestrictContended at runtime. This annotation no longer works after 1.9 )

orderliness

        All the word order in the program is not what we think can be completed by one operation. It is composed of multiple instructions at the underlying bytecode level. In order to improve the execution efficiency, the CPU does not affect the final consistency of single-threaded execution. The instructions will be reordered under the premise. Here is a typical example to illustrate:

        Object object = new Object(); This statement can be regarded as an operation to create an object object in a java program, so how is it implemented at the bytecode level, as shown in the following figure (using the jcalssLib plug-in):

It is composed of the above five instructions, and the meaning of the specific instructions can be viewed in the official document. It is roughly divided into three steps: 1) Apply for memory space and assign default values; 2) Execute the construction method to assign initial values; 3) Point the memory to the object. When the cpu executes, it does not necessarily follow the order of 1->2->3, and the execution order may become 1->3->2. There is no problem in a single thread, because the final result It is an object with an initial value assigned. But if it is in multi-threading, there may be problems. Take the following DCL code as an example:

public class Single {
    private volatile static Single SINGLE;
    private Single(){ }
    public static Single getInstance(){
        if (SINGLE == null){
            synchronized (Single.class){
                if (SINGLE == null){
                    SINGLE = new Single();
                }
            }
        }
        return SINGLE;
    }
}

 Is the keyword volatile in the above code necessary? The answer is yes, if there is no such keyword, there may be out-of-order execution that may cause problems in the final result. Now there are two threads, one thread executes to the SINGLE = new Single() method, and the other thread executes to the first Empty judgment, if the newly created object is out of order, execute step 3 in the order of 1->3->2, and the second thread cashback object is not empty, it will use the SINGLE object that only serves the default index , there may be problems, so the volatile keyword in DCL is a must.

        So how is the order guaranteed (see above for details):

        Hardware level: cpu uses cpu-level memory barriers to implement: lfence, sfence, mfence;

        Jvm level: The virtual machine uses the memory barrier at the virtual machine level to realize: LoadLoad, StoreStore, LoadStore, StoreLoad. It should be noted that the memory barrier at the virtual machine level is completed by calling related instructions at the hardware level, such as the lock instruction.

atomicity

        Before understanding atomicity, first understand a few concepts: race condition: competition condition, competition occurs when multiple threads access shared data; monitor: lock; critical section: critical section, code executed when a thread holds a lock; lock granularity: The execution time of the critical section is long and there are many statements, which means that the lock granularity is relatively coarse; otherwise, the lock granularity is relatively fine.

        The so-called atomicity means that the execution of this code cannot be interrupted. Before the current thread has finished executing, other threads cannot interrupt the execution of the thread code. Atomicity can be guaranteed by locking the code.

        There are two types of locks:

        1. Pessimistic lock: Mainly synchronized, no matter whether the lock may occur or not, lock it directly.

        2. Optimistic lock: Represented by CAS spin lock, it will first default that the current code does not need to be locked, but when it is finally submitted, it will judge whether there is thread competition. If there is no thread competition, the execution ends; otherwise, it needs to be locked. Re-run.

        Regarding the concept of the above two locks, they have their own suitable usage scenarios: because of the cas spin lock, the thread needs to spin and wait, which will waste cpu resources, so the execution time of the critical section is long, and when there are many threads competing for it, use the synchronized pessimistic lock; If the execution time of the critical section is short and there are few contending threads, use the CAS optimistic lock.

        Synchronized and CAS are mentioned above, and then these two locks will be introduced.

synchronized

        Synchronized can guarantee visibility and atomicity, but it cannot guarantee order. Before optimization, it is a heavyweight lock, and each lock is completed by the operating system; after optimization, it is more efficient, no longer a lock is applied for a lock to the operation, it will perform a series of lock upgrades process.

        Before introducing the lock upgrade process, you need to know that the relevant information of the lock is recorded in the mark word, so we are also required to not modify the reference object when using a reference object as a lock, so as to avoid the change of the mark word of the object and cause the lock to become invalid .

        The normal lock upgrade process of synchronized: no lock->biased lock->lightweight lock (spin lock)->heavyweight lock. Among them, biased locks and lightweight locks are located in user space, while heavyweight locks apply to kernel space.

        When a thread executes the critical section of the synchronized lock, the lock-free object is upgraded to a biased lock, and the current thread pointer will be recorded on the mark word of the lock, and no other operations will be performed. The hashcode value recorded on the mark word will be put into the thread stack record; when the execution of the thread is not completed, and there are less than 10 threads that are less than half of the number of CPU cores competing for the lock, the biased lock will be upgraded to a lightweight lock. At this time, the lock of the biased lock will be eliminated first. , the competing threads will generate LR (lock record) in their own thread stack, whoever records their LR on the lock mark word first will obtain the biased lock, and other threads will start to spin and wait; when After more than 10 competing threads, the lightweight lock is upgraded to a heavyweight lock. At this time, a lock application will be made to the operating system, and the threads competing for the lock will be put into the relevant queue of the operating system, and the thread that has acquired the lock will run normally. And other threads are counted in the waiting queue waiting for the lock to be released. The above is the whole process of lock upgrade. Of course, this process is not necessary. In some special cases, the lock upgrade process will be changed.

        When the biased lock has not been activated, the lock upgrade process is: lock-free object -> lightweight lock -> heavyweight lock. You can use the parameter -XX:BiasedLockingStartupDelay=4000 to set the delay time for starting the biased lock, and the default is 4s.

        Synchronized is a reentrant lock. The so-called reentrant lock means that thread 1 acquires lock a and needs to acquire lock a again before releasing the lock. If it is allowed, it is a reentrant lock; if it is not allowed, it is not reentrant Lock. Since synchronized is a reentrant lock, it is necessary to record the number of reentries, because it is necessary to unlock the corresponding number of times according to the number of reentries. For biased locks and lightweight locks, the number of reentries is recorded by putting LR into the thread stack. Heavyweight locks record reentrancy information into the operating system.

CAS optimistic lock

        The cas (comapre and swap) spin lock is executed in user space and does not involve kernel space. One of his general workflows is advanced critical section code execution. After the execution is completed, the current value is compared with the expected value. If it passes, the current value is updated to the expected value. After the execution ends, if it is inconsistent, the above process is executed in a loop. As shown below:

When using CAS, there may be an ABA problem, that is, the C obtained by the current thread is the C updated after other threads are processed. In order to solve this problem, it can be solved by adding a version number. If it is a basic data type, the ABA problem may not affect the normal operation of the business, but it should be noted that if it is a reference object type, you cannot only compare the addresses when comparing, and you must re-equal method, otherwise problems will occur.

        atomic atomic class, the related operations inside are atomic, and its atomicity is realized by using CAS optimistic lock. Its CAS depends on the implementation of the assembly instruction cmpxchg. If it is a multi-core CPU, you need to add a lock instruction to lock, because the cmpxchg instruction itself is not atomic, so it needs to be locked.

Guess you like

Origin blog.csdn.net/weixin_38612401/article/details/123916565