JAVA multithreading analysis of various problems

Before we begin, we need to mention the pre-chapter

You can learn more about this section

  1. Basic Concepts of JAVA concurrency
  2. JAVA concurrent processes VS threads

First of all, let’s talk about the advantages of concurrency, according to the advantages of the characteristics, lead to concurrency should pay attention to the security issues

1 Advantages of concurrency

Technology is advancing, and the performance of CPU, memory, and I/O devices is improving. However, there is always a core contradiction: speed differences between CPU, memory, and I/O devices. CPU is much faster than memory, and memory is much faster than I/O devices.

According to the short board theory, how much water can be held in a barrel depends on the shortest piece of wood. Overall program performance depends on the slowest operation, I/O, which means improving CPU performance unilaterally is not effective.

In order to make reasonable use of the high performance of CPU and balance the speed differences among the three, the computer system, operating system and compiler have all made contributions, which are mainly reflected as follows:

  • The CPU added a cache to balance the speed difference with memory.
  • The operating system added processes and threads to time-share multiplexing CPU, and then balance the speed difference between CPU and I/O device;
  • The compiler optimizes the order of instruction execution so that the cache can be used more efficiently.

Among them, the process, thread makes the computer, the program has the ability of concurrent processing tasks, it has two important advantages:

  • Improving resource Utilization
  • Reduce application response time

1.1 Improving Resource Utilization

When reading a file from disk, most of the CPU time is spent waiting for disk to read the data. During this time, the CPU is very idle. It can do something else. By changing the order of operations, CPU resources can be better used. The concurrent mode is not necessarily disk I/O, but can also be network I/O and user input, etc., but either type of I/O is much slower than CPU and memory I/O. Threads do not increase speed, but rather perform some time-consuming function while doing other things. Multithreading allows your program to process files without appearing to be stuck.

1.2 Reduce program response time

Another common goal of turning a single-threaded application into a multi-threaded application is to achieve a more responsive application. Imagine a server application that listens for incoming requests on a port. When a request comes in, it processes the request and then goes back to listen.

The flow of the server is as follows:

public class SingleThreadWebServer {
    public static void main(String[] args) throws IOException{
        ServerSocket socket = new ServerSocket(80);
        while (true) { Socket connection = socket.accept(); handleRequest(connection); }}}Copy the code

If a request takes a lot of time to process, the new client cannot send the request to the server during this time. Requests can only be received if the server is listening. In another design, the listener thread passes the request to the worker thread and then immediately returns to listen. The worker thread can process the request and send a reply to the client. This design is described as follows:

public class ThreadPerTaskWebServer {
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (true) {
            final Socket connection = socket.accept();
            Runnable workerThread = new Runnable() {
                public void run(a) {handleRequest (connection); }}; }}}Copy the code

This way, the server thread quickly returns to listen. As a result, more clients can send requests to the server. The service has also become more responsive.

The same is true for desktop applications. If you click on a button to start running a time-consuming task, the thread will both perform the task and update Windows and buttons, and the application will appear unresponsive while the task is being executed. Instead, tasks can be passed to worker threads. While the worker thread is busy working on tasks, the window thread is free to respond to requests from other users. When the worker thread completes a task, it sends a signal to the window thread. The window thread updates the application window and displays the results of the task. Programs with worker thread designs appear to be more responsive to users.

2 Security issues caused by concurrency

Concurrency security refers to the following three characteristics:

** atomicity :** in popular terms, operations are not interrupted by other threads in the middle of the process. This is usually implemented by a synchronization mechanism (sychronized, Lock).

** Orderliness :** ensures serial semantics in threads, avoids instruction reordering, etc

Visibility: When a thread changes a shared variable, its status is immediately known to other threads, usually interpreted as reflecting thread-local state onto main memory,volatileIs responsible for ensuring visibility

Ps: Volatile is a volatile keyword that needs to be addressed in a separate article. Keep an eye out for updates

2.1 Atomicity problem

In the early days,CPU speeds were much faster than IO operations. When a program read a file, it could mark itself as a “sleep state” and surrender CPU usage. After data was loaded into memory, the operating system would wake up the process and then have a chance to regain CPU usage. These operations cause a switch between processes. Different processes do not share memory space, so the process must switch the memory mapping address to perform the task switch. And all the threads created by a process share the same memory space, so it’s very cheap for threads to do task switching so when we’re talking about task switching we’re talking about thread switching

A statement in a high-level language usually requires multiple CPU instructions to complete, such as:

Count += 1 requires at least three CPU instructions

  • Instruction 1: First, we need to load the variable count from memory into the CPU register;
  • Instruction 2: After that, the +1 operation is performed in the register;
  • Instruction 3: Finally, write the result to memory (the caching mechanism makes it possible to write to the CPU cache instead of memory).

Atomic problems arise:

For the above three instructions, we assume that count=0. If thread A switches after instruction 1 completes, thread A and thread B follow the sequence shown below, then we see that both threads perform count+=1, but get 1 instead of the expected 2.

We call atomicity the property of one or more operations being executed by the CPU without interruption. The atomic operations guaranteed by the CPU are CPU instruction level, not high-level language operators, which is counterintuitive. Therefore, many times we need to ensure atomicity of operations at the high-level language level.

2.2 Orderliness

As the name implies, orderliness means that programs are executed in order of code. The compiler sometimes changes the order of statements in a program to optimize performance

Here’s an example:

In the getInstance() method, we first determine if instance is empty. If so, we lock singleton.class and check again if instance is empty. If still empty, create an instance of the Singleton.

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(a){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = newSingleton(); }}returninstance; }}Copy the code

If threads A and B call getInstance() at the same time, they will check that instance is null and lock the singleton.class. At this point, the JVM will guarantee that only one lock is locked successfully and the other thread will wait. If thread A successfully locks the instance, thread A will create an instance and release the lock. Thread B will wake up and lock the instance again. If the instance is successfully locked, thread B will check whether the instance is null and will find that it has been instantiated and will not create another instance.

The code and logic look fine, but the getInstance() method is actually a problem. The problem is with the new operation, which we think should be:

1. Allocate memory

2. Initialize the Singleton object on this memory

3. Assign the memory address to the instance variable

But the actual JVM optimized operation looks like this:

1 Allocating Memory

2 Give the address to the instance variable

3 Initialize the Singleton object in memory

The optimization will cause that when another thread accesses the instance member variable, it will terminate the instantiation operation and return the instance without null, which will trigger the null pointer exception.

2.3 Visibility issues

Changes made by one thread to a shared variable that another thread can see immediately are called visibility.

In modern multi-core cpus, each core has its own cache, and when multiple threads execute on different CPU cores, the threads operate on different CPU caches.

Example of thread insecurity

The code below loops through count+=1 10,000 times for each execution of the add10K() method. In the calc() method, we create two threads, each calling add10K() once. Let’s think about the result of executing calc().

class Test {
    private static long count = 0;
    private void add10K(a) {
        int idx = 0;
        while(idx++ < 10000) {
            count += 1; }}public static  long getCount(a){
        return count;
    }
    public static void calc(a) throws InterruptedException {
        final Test test = new Test();
        // Create two threads and add()
        Thread th1 = new Thread(()->{
            test.add10K();
        });
        Thread th2 = new Thread(()->{
            test.add10K();
        });
        // Start two threads
        th1.start();
        th2.start();
        // Wait for both threads to finish executing
        th1.join();
        th2.join();
    }

    public static void main(String[] args) throws InterruptedException {
        Test.calc();
        System.out.println(Test.getCount());
        // Run three times to output 11880 12884 14821 respectively}}Copy the code

Our intuition tells us it should be 20000, because if we call add10K() twice in a single thread, count will be 20000, but in reality calc() will execute a random number between 10000 and 20000. Why is that?

If thread A and thread B start at the same time, the first time they both read count=0 into their respective CPU caches, the second time they read count+=1, the second time they read count+=1 into their respective CPU caches, and the second time they write count+=1 into memory, they find that they have 1 instead of 2. After that, both threads calculated the count value based on the count value in the CPU cache, so the final count value was less than 20000. This is the visibility of the cache.

If you loop 10,000 times count+=1 instead of 100 million times, you’ll see that the effect is more pronounced and the final count is closer to 100 million than it is to 200 million. If you loop 10000 times, count is close to 20000 because the two threads are not started at the same time and there is a time difference.

3 How to ensure concurrency security

To understand how to ensure concurrency safety, first understand what synchronization is:

Synchronization refers to ensuring that shared data is accessed by only one thread at a time when multiple threads concurrently access the shared data

Concurrency security can be implemented in three ways:

1. Blocking synchronization (pessimistic locking):

Blocking synchronization, also known as Mutex synchronization, is a common means to ensure the correctness of concurrency. Critical Sections, Mutex and Semaphore are the main ways to implement Mutex

The most typical example is using synchronized or Lock.

The main problem with mutex synchronization is the performance problems caused by thread blocking and waking up. Mutex synchronization is a pessimistic concurrency strategy that assumes that if you don’t do the right synchronization, you’re going to have a problem. Whether or not shared data is actually competing, it does locking (this is a conceptual model, but the virtual machine optimizes a large portion of unnecessary locking), user-mode core mind-shifting, maintaining lock counters, and checking to see if there are blocked threads that need to be woken up.

2. Non-blocking synchronization (Optimistic locking)

An optimistic concurrency strategy based on collision detection: do the operation first, if no other threads compete for the shared data, then the operation succeeds, otherwise compensate (keep retrying until it succeeds). Many implementations of this optimistic concurrency strategy do not require threads to block, so this synchronization operation is called non-blocking synchronization

Optimistic lock instructions are common:

  • Test and Set (test-AMD-set)
  • Fetch and Increment
  • Swap
  • Compare and exchange (CAS)
  • Load linked/store-conditional

Atomic classes in the J.U.C package (based on the Unsafe class for CAS (Compare and swap) operations)

3. No synchronization

Synchronization is not necessary to be thread-safe. Synchronization only ensures the correctness of shared data contention. If a method does not involve shared data, synchronization is not necessary.

Non-synchronization schemes in Java include:

  • Reentrant code – also called pure code. A method that returns the same predictable result as long as it inputs the same data is reentrant, can continue to execute at the point of interruption, and is of course thread-safe.
  • Thread-local storage – Using ThreadLocal creates a local copy of shared variables in each thread. This copy can only be accessed by the current thread and not by other threads, so it is naturally thread-safe.

4 summarizes

For the sake of concurrent advantages We chose the multithreading, benefits the multithreading and send us also poses a problem, handle these security problems we choose lock to share data into only one thread at the same time to ensure that the concurrent data security, then lock also brings many problems for us Such as: a deadlock, live lock, thread hunger and other problems

Look forward to the next article where we will analyze the activity problems caused by locking

Concern public number: Java baodian