Advanced Java--Advantages & Disadvantages of Concurrent Programming

Table of contents

Pros & Cons of Concurrent Programming

Why use concurrent programming (advantages):

Disadvantages of concurrent programming:

frequent context switching

thread safety

confusing concepts

blocking and non-blocking

blocking model

non-blocking model

synchronous and asynchronous

synchronous call

asynchronous call

critical section

Concurrency and Parallelism

context switch


Pros & Cons of Concurrent Programming

Concurrent programming refers to the simultaneous execution of multiple independent tasks or operations in a program to improve the performance and responsiveness of the program.

Why use concurrent programming (advantages):

  1. Improve system performance: Through concurrent programming, the computing power of multi-core processors can be fully utilized to achieve parallel execution of tasks and improve system throughput and response speed.
  2. Improve code efficiency: Using concurrent programming technology can execute tasks asynchronously, reduce waiting time, improve code execution efficiency, and improve the overall performance of the system.
  3. Enhance the scalability of the program: Through concurrent programming, tasks can be split into multiple small tasks and processed in parallel, which is convenient for adding, adjusting or deleting tasks flexibly, so as to realize the easy scalability and modular design of the system.
  4. Improve user experience: Concurrent programming can make the program more responsive, avoid interface freezing or blocking, and improve user interaction experience.

Disadvantages of concurrent programming:

  1. High complexity of multi-threaded programming: Concurrent programming needs to consider issues such as thread safety, race conditions, deadlocks, etc. The requirements for developers are high, and it is more difficult to write and debug concurrent code.
  2. Bugs are prone to occur: due to the competition conditions between threads and access to shared resources in concurrent programming, incorrect concurrent codes can easily lead to data inconsistency, deadlock and other problems, increasing the risk of errors in the program.
  3. Performance loss: The creation and context switching of threads will bring a certain amount of overhead. If concurrent programming improperly uses or manages too many threads, it will consume system resources and affect performance.

frequent context switching

Context switching means that when the operating system is executing a certain task, it needs to switch to another task for some reason, save the context of the current task (that is, state information) and load the context of another task, and then start executing the task. This process incurs a certain performance overhead because context information needs to be saved and loaded.

In multi-threaded programming, frequent context switching may lead to performance degradation, because switching between threads requires saving and loading thread context information, which is very time-consuming. Therefore, we need to take some measures to reduce the number of context switches in order to give full play to the advantages of multi-threaded programming.

  1. Lock-free concurrent programming: Using the idea of ​​lock segmentation, different threads process different segments of data, reducing the context switching time in the case of multi-thread competition.
  2. CAS algorithm: Use atomic operations (CAS) to update data, use optimistic locks, and reduce context switching caused by unnecessary lock competition.
  3. Use the least number of threads: Avoid creating unnecessary threads, such as avoiding creating too many threads when there are few tasks, so as to reduce a large number of threads in a waiting state.
  4. Coroutine: implement multi-task scheduling in a single thread, and perform task switching in a single thread.

Note: Context switching is also a relatively time-consuming operation. An experiment in the book "The Art of Concurrent Programming in Java" shows that concurrent accumulation is not necessarily faster than serial accumulation. You can use tools such as Lmbench3 to measure the duration of context switches, and use vmstat to measure the number of context switches.

The following is a simple Java code example that demonstrates how to use lock segmentation to reduce the number of context switches:

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

public class Main {
    private static final int SEGMENT_COUNT = 16; // 锁分段数量
    private final Lock[] segmentLocks; // 锁分段数组
    private final int[] data; // 数据数组

    public Main() {
        // 初始化锁分段数组和数据数组
        segmentLocks = new ReentrantLock[SEGMENT_COUNT];
        for (int i = 0; i < SEGMENT_COUNT; i++) {
            segmentLocks[i] = new ReentrantLock();
        }
        data = new int[SEGMENT_COUNT];
    }

    private Lock getSegmentLock(int index) {
        // 根据索引计算锁在锁分段数组中的位置
        int segmentIndex = Math.abs(index % SEGMENT_COUNT);
        return segmentLocks[segmentIndex];
    }

    /**
     * 更新数据
     *
     * @param index 索引
     * @param value 值
     */
    public void updateData(int index, int value) {
        Lock lock = getSegmentLock(index);
        lock.lock(); // 获取锁
        try {
            data[index] = value; // 更新数据
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    /**
     * 获取数据
     *
     * @param index 索引
     * @return 数据值
     */
    public int getData(int index) {
        Lock lock = getSegmentLock(index);
        lock.lock(); // 获取锁
        try {
            return data[index]; // 返回数据
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public static void main(String[] args) {
        Main main = new Main();

        // 示例调用 updateData() 和 getData() 方法
        main.updateData(0, 1);
        int value = main.getData(0);
        System.out.println("价值: " + value);//价值: 1
    }
}

thread safety

The most difficult thing to grasp in multi-threaded programming is the thread safety issue of the critical section. A deadlock will occur if you don't pay attention to it. Once a deadlock occurs, the system function will be unavailable.

In order to avoid the deadlock situation, the following measures can be taken:

  1. Avoid a thread to acquire multiple locks at the same time, avoiding the risk of deadlock.
  2. Each lock only occupies one resource, preventing a thread from occupying multiple resources inside the lock.
  3. Use a timed lock, use the tryLock() method to try to acquire the lock, and release the lock after a timeout to avoid threads waiting indefinitely.
  4. For database locks, ensure that the locking and unlocking operations must be performed within the same database connection to avoid unlocking failures.

Also, it is important to understand the issues of atomicity, ordering, and visibility of the JVM memory model. For example, data dirty reading means that a thread modifies the value of a shared variable, but it has not been flushed to the main memory, and another thread still sees the old value when reading the variable. DCL (Double Check Lock) is an optimization technique used to reduce the overhead of locks, but it may cause data races and thread safety issues.

Learning multi-threaded programming technology requires a deep understanding of the concepts and principles of concurrent programming, mastering various concurrent tools and technologies, and being able to choose appropriate solutions according to specific scenarios. Through learning and practice, you can improve your concurrent programming ability and program performance, and at the same time improve your understanding and mastery of multi-threaded programming.

When it comes to critical section thread safety issues, here is an example of Java code that uses locking mechanism to ensure thread safety: 

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

public class Main {
    private int count = 0;
    private Lock lock = new ReentrantLock(); // 创建一个可重入锁

    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++; // 临界区,对共享变量进行操作
        } finally {
            lock.unlock(); // 释放锁,确保在发生异常时也能正常释放锁
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        Main example = new Main();

        // 创建多个线程并执行increment方法
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    example.increment();
                }
            });
            thread.start();
        }

        // 等待所有线程执行完毕
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出计数结果
        System.out.println("计数= " + example.getCount());//运行结果:计数= 5000
    }
}

confusing concepts

blocking and non-blocking

Blocking and non-blocking are important concepts in concurrent programming. They describe the behavior of a thread while waiting for an operation to complete.

blocking model

In the blocking model, when a thread calls an operation, if the operation has not been completed, the thread will be suspended and enter the waiting state until the operation is completed. During this process, the thread will not perform other tasks, so the blocking model is a way of busy-waiting. In Java, some I/O operations (such as reading a file or a network connection) are blocking, and if the operation has not yet completed, the thread will be suspended until the operation is completed.

non-blocking model

The non-blocking model does not suspend threads. In the non-blocking model, after a thread invokes an operation, a result is returned immediately, regardless of whether the operation is completed or not. The result may be a partial completion of the operation or an error status. During this process, the thread can continue to perform other tasks, so the non-blocking model can improve the concurrency and efficiency of the system. In Java, some concurrent programming tool classes such as Lock and Semaphore in java.util.concurrent are non-blocking, and they provide non-blocking thread synchronization and counting operations.

In practice, both blocking and non-blocking models have advantages and disadvantages.

  1. The blocking model is simple and easy to understand, but may lead to inefficiencies in the system.
  2. The non-blocking model can improve the concurrency and efficiency of the system, but may require more complex logic to process the results and status of operations.

synchronous and asynchronous

In computer programming, synchronous and asynchronous are the two main ways of dealing with tasks and data flow.

synchronous call

A synchronous call is a blocking call, which means that the caller will wait for the callee to complete the operation and return the result. While the caller is waiting, its thread is blocked and cannot perform other tasks. Only when the callee completes the task and returns the result, the caller will continue to execute subsequent code. In the synchronous model, there is a clear request-response relationship between the caller and the callee, and the caller must wait for the response before proceeding to the next step.

asynchronous call

An asynchronous call is a non-blocking call, meaning that the caller returns immediately after sending the request without waiting for the callee to complete the operation. The caller can continue to perform other tasks without waiting for the callee to complete. In the asynchronous model, there is no clear request-response relationship between the caller and the callee, but the results are delivered through callback functions or event notifications.

There are generally two ways to get the result of an asynchronous call:

  1. Actively poll the result of the asynchronous call;
  2. The callee notifies the caller of the call result through callback;

The advantage of asynchronous calls is that they can increase the concurrency and efficiency of the system, because the caller can perform other tasks while waiting for the result. However, the asynchronous model also has some disadvantages, such as it may require more complex logic to handle callback functions and result notifications, and it may face some concurrency issues, such as race conditions and deadlocks.

In practical applications, whether to choose synchronous or asynchronous depends on specific needs and scenarios. For operations that require immediate results, synchronous calls are usually used; for scenarios that do not require immediate results, such as long-term I/O operations or network requests, asynchronous calls are usually used to improve system efficiency and performance.

critical section

A critical section is a programming concept used to manage access to shared resources. In multithreaded programming, multiple threads may attempt to access and modify shared data concurrently, which can lead to data inconsistencies and other concurrency issues. Critical sections avoid these problems by providing a way to ensure that only one thread can access a shared resource at any given time.

A critical section is usually an area defined in a piece of code that contains operations that require access to shared resources. Before entering a critical section, a thread must acquire a lock to prevent other threads from entering the critical section at the same time. When a thread leaves the critical section, it must release the lock, allowing other threads to enter the critical section.

The use of critical sections can ensure data consistency and concurrency control. It also prevents data races and other concurrency issues such as deadlocks and starvation. However, the use of critical sections may also cause blocking and waiting between threads, which may affect the performance and responsiveness of the program. Therefore, when using a critical section, you need to weigh its advantages and disadvantages and make a decision based on the actual situation.

In programming, there are many different synchronization primitives (synchronization primitives) that can implement the functions of critical sections, such as mutexes, read-write locks, semaphores, etc.

Concurrency and Parallelism

  1. Concurrency refers to the alternate execution of multiple tasks within a period of time. These tasks may partially overlap, but they won't really execute concurrently. In the era of single-core CPUs, concurrency is achieved through time slice rotation, that is, the CPU alternately executes multiple tasks, each task executes for a period of time, and then switches to another task. That way, while each task is not truly executing simultaneously, from the user's perspective they appear to be doing so.
  2. Parallel means that at the same time, multiple tasks are executed at the same time. This requires multi-core CPU or multi-threaded environment support. Parallelism can greatly improve the execution efficiency of the program, especially when a large amount of calculation or data needs to be processed.
  3. Serial refers to the sequential execution of multiple tasks or methods in one thread. This means that tasks or methods are executed one after the other, with no overlapping parts. Serial execution is common in single-threaded environments, such as programs that use a single thread or certain functions or methods that execute serially.

context switch

Context switching is an important concept in multi-threaded programming. It means that when the time slice of a thread runs out, the CPU will save the state of the thread, and then load another thread in the ready state and execute it. This process is a context switch.

The purpose of context switching is to allow multiple threads to share CPU resources, and each thread can get a certain amount of execution time. Since a CPU core can only be used by one thread at any time, the CPU takes a round-robin approach to assign time slices to each thread.

The process of context switching includes saving the state of the current thread (including register state, variables in memory, etc.) and loading the state of the thread in the next ready state. This process takes a certain amount of time, and as the number of threads in the system increases, the number of context switches also increases, which consumes a lot of CPU time.

In operating systems such as Linux, the time consumption of context switching and mode switching is relatively small, which is also an important reason for the excellent performance of Linux. This is because Linux uses many optimization techniques, such as using the kernel scheduler, using hardware interrupts, etc., to reduce the time consumption of context switching and mode switching.

Guess you like

Origin blog.csdn.net/m0_74293254/article/details/132507824