Is multithreading fast?

1. Concurrent programming and multi-threaded programming

  To understand concurrent programming, we must first understand the distinction between parallelism. Parallel refers to two events happening at the same time, concurrency means that the CPU switches fast, and it looks like every task is going on at the same time. Multithreading is a way to implement concurrent programming. Suppose a scenario where a group of people flock into the subway during the peak hours of the Guangzhou subway and swipe in at different gates. In this scenario, entering the subway is a task, everyone can see that it is concurrent, and multiple swipe gates are multi-threaded.

  The essential purpose of concurrent programming is to make full use of the CPU and make the program run faster. However, it is not that starting more threads will allow the program to execute concurrently to the maximum. In concurrent programming, if you want to perform tasks through multiple threads to make the program run faster, you will face many challenges. For example, the problem of context switching, the problem of deadlock, and the problem of resource limitation due to hardware and software, the following are the factors that whizz.

2. Context switch

2.1 Principle analysis

  As mentioned above, the biggest difference between concurrency and parallelism is that concurrency just looks like parallelism. In fact, the CPU allocates time to each thread to execute the program of this thread, but this time is very short, usually tens of milliseconds, we can not observe the change at all, it feels that they are all executed simultaneously.

  The CPU cyclically executes tasks through the time slice allocation algorithm. After the current task executes a time slice, it switches to the next task. However, the state of the previous task will be saved before switching, so that the next time you switch back to this task, you can load the state of this task again. So the task from saving to reloading is a context switch. Therefore, it is not difficult to know that context switching takes a lot of time.

  Let's assume a scenario where a person goes to a train station to buy tickets. There are as many as ten windows for buying tickets. The ticket buyer does not know which window can buy the ticket, and can only ask one by one, and finally bought it in the last window. In this scenario, it seems that the process of buying tickets is very long. In fact, most of the time is on the switch window, which is the problem of context switching. Therefore, it is not necessary to execute fast with a large number of threads. It is the best solution to choose the number of threads suitable for the task.

2.2, test code

package Concurrency;

/**
 * @author RuiMing Lin
 * @date 2020-03-28 12:19
 */
public class Demo1 {
    public static void main(String[] args) {
        System.out.println("万级循环:");
        concurrency(10000);
        serial(10000);
        System.out.println("--------------------------华丽分隔符--------------------------------");
        System.out.println("十万级循环:");
        concurrency(100000);
        serial(100000);
        System.out.println("--------------------------华丽分隔符--------------------------------");
        System.out.println("百万级循环:");
        concurrency(1000000);
        serial(1000000);
        System.out.println("--------------------------华丽分隔符--------------------------------");
        System.out.println("千万级循环:");
        concurrency(10000000);
        serial(10000000);
        System.out.println("--------------------------华丽分隔符--------------------------------");
        System.out.println("亿级循环:");
        concurrency(100000000);
        serial(100000000);
    }

    private static void concurrency(long count){
        // 开启三个线程执行三个循环
        long start = System.currentTimeMillis();
        new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (long i = 0; i < count; i++) {
                    a++;
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                int b = 0;
                for (long i = 0; i < count; i++) {
                    b++;
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                int c = 0;
                for (long i = 0; i < count; i++) {
                    c++;
                }
            }
        }).start();
        long end = System.currentTimeMillis();
        long time = end - start;
        System.out.println("并行执行花费时间为:" + time + "ms");
    }

    private static void serial(long count){
        // 三个循环顺序执行
        long start = System.currentTimeMillis();
        int a = 0;
        int b = 0;
        int c = 0;
        for (int i = 0; i < count; i++) {
            a++;
        }
        for (int i = 0; i < count; i++) {
            b++;
        }
        for (int i = 0; i < count; i++) {
            c++;
        }
        long end = System.currentTimeMillis();
        long time = end - start;
        System.out.println("串行执行花费时间为:" + time + "ms");
    }
}

Results output:

Ten thousand levels of loops:
parallel execution takes time: 4ms
serial execution takes time: 1ms
-------------------------- gorgeous separator- ------------------------------
One hundred thousand level loop:
parallel execution takes time: 1ms
serial execution takes time: 4ms
-------------------------- Gorgeous separators --------------------- -----------
Million-level loop:
parallel execution time: 1ms
serial execution time: 10ms
------------------- ------- Gorgeous separators --------------------------------
Ten-million-level loop:
parallel execution cost Time is: 1ms
Serial execution takes time: 36ms
-------------------------- Gorgeous separator --------- -----------------------
Billion-level loop:
parallel execution time: 1ms
serial execution time: 357ms

Analysis result: When the order of magnitude is in the tens of thousands, the serial is faster than the concurrency. When the order of magnitude reaches 100,000, the serial becomes weak. Therefore, it can be considered that when the program execution volume is not large enough, it is not necessary to enable multithreading.

2.3. How to reduce context switching

Methods to reduce context switching include lock-free concurrent programming, CAS algorithm, use of minimum threads, and use of coroutines.

  1. Concurrency-free programming. When multiple threads compete for locks, it will cause context switching. Therefore, when processing data in multiple threads, you can use some methods to avoid using locks. For example, the ID of the data is segmented according to the Hash algorithm, and different threads process different segments of data.
  2. CAS algorithm. Java's Atomic package uses the CAS algorithm to update data without locking.
  3. Use the fewest threads. Avoid creating unnecessary threads, for example, there are few tasks, but many threads are created for processing, which will cause a large number of threads to be in a waiting state.
  4. Coroutine: Implement multi-task scheduling in a single thread and maintain switching between multiple tasks in a single thread.

3. Deadlock

3.1 Principle analysis

  Deadlock refers to a deadlock caused by multiple threads competing for the same resource during running. When the process is in this deadlock state, they will no longer be able to move forward, and the program is in a paralyzed state at this time. carried out. Usually, multiple threads compete for the same lock object, and an exception occurs after one of the threads acquires the lock, and the lock can be released in the future, causing the other threads to wait all the time and fail to run.

3.2, test code

package Concurrency;

/**
 * @author RuiMing Lin
 * @date 2020-03-28 13:14
 */
public class Demo2 {
    private static String str1 = "A";
    private static String str2 = "B";

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (str1){
                    System.out.println("第一个线程获得str1");
                    try {
                        Thread.currentThread().sleep(2000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    synchronized (str2){
                        System.out.println("第一个线程获得str2");
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (str2){
                    System.out.println("第二个线程获得str2");
                    try {
                        Thread.currentThread().sleep(2000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    synchronized (str1){
                        System.out.println("第二个线程获得str1");
                    }
                }
            }
        }).start();
    }
}

Result output:
Insert picture description here
Analysis result: After the first thread acquires lock str1, the thread falls asleep for 2000ms. At this time, the second thread is executed, and the second thread acquires the lock str2. At this time, since the lock str1 is not released, the second thread is blocked and cannot be executed. So, switching to the first thread again, it is also unable to obtain lock str2, blocking.

3.3, how to solve the deadlock

  1. Avoid one thread acquiring multiple locks at the same time.
  2. Avoid one thread occupying multiple resources in the lock at the same time, try to ensure that each lock only consumes one resource.
  3. Try to use time lock, use lock.tryLock (timeout) instead of using internal lock mechanism.
  4. For database locks, locking and unlocking must be in a database connection, otherwise unlocking will fail.

4. Summary

  Concurrent programs are not simple programs, they should be rigorous in writing. Complex code is likely to cause deadlocks, so it is recommended to use the concurrent containers and tools provided by JDK concurrent packages to solve concurrency problems. At the same time, we must also pay attention to the problem of new energy, not only considering the amount of program execution tasks, but also considering CPU performance, etc., and not increasing the number of threads.

Please point out any mistakes, welcome to comment or private message exchange! Keep updating Java, Python and big data technology every day, please pay attention!

Published 64 original articles · Like 148 · Visitors 20,000+

Guess you like

Origin blog.csdn.net/Orange_minger/article/details/105158557