It’s numb, the code is changed to multi-threaded, there are 9 major problems

Time:2022-9-23

foreword

In many cases, in order to improve the performance of the interface, we willsingle thread synchronizationThe code to execute is changed toMulti-threaded asynchronousimplement.

For example, the interface for querying user information needs to return basic user information, point information, and growth value information, while users, points, and growth value need to call different interfaces to obtain data.

If you query the user information interface,Synchronous callIt is very time-consuming to obtain data through three interfaces.

It is very necessary to change the three interface calls toAsynchronous call,at lastAggregate Results

Another example is the registration user interface, which mainly includes functions such as writing user tables, assigning permissions, configuring user navigation pages, and sending notification messages.

The user registration interface contains a lot of business logic. If these codes are executed synchronously in the interface, the response time of the interface will be very slow.

At this time, you need to sort out the business logic and divide it into:core logicandnon-core logic. The core logic in this example is: writing the user table and assigning permissions, and the non-core logic is: configuring the user navigation page and sending notification messages.

obviouslycore logicmust be in the interfaceSynchronous execution,andnon-core logicCanMulti-threaded asynchronousimplement.

and many more.

There are too many business scenarios that require the use of multithreading, and the benefits of using multithreading for asynchronous execution are self-evident.

But what I want to say is that if multithreading is not used well, it will also bring us a lot of unexpected problems. If you don’t believe it, continue to look at it later.

Today, I will talk with you about the 9 major problems that arise after the code is changed to multi-threaded calls.

1. Can’t get the return value

If you pass direct inheritanceThreadclass, or implementationRunnableinterface to createthread

Well, congratulations, you won’t be able to get the return value of the thread method.

There are two scenarios for using threads:

  1. There is no need to pay attention to the return value of the thread method.
  2. Need to pay attention to the return value of the thread method.

Most business scenarios do not need to pay attention to the return value of the thread method, but what if we need to pay attention to the return value of the thread method for some business?

The interface for querying user information needs to return basic user information, point information, and growth value information, while users, points, and growth value need to call different interfaces to obtain data.

As shown below:

Before Java 8 can be achieved byCallableInterface to get the thread return result.

Passed after Java8CompleteFutureclass implements this functionality. Here we take CompleteFuture as an example:

public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException {
    final UserInfo userInfo = new UserInfo();
    CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> {
        getRemoteUserAndFill(id, userInfo);
        return Boolean.TRUE;
    }, executor);

    CompletableFuture bonusFuture = CompletableFuture.supplyAsync(() -> {
        getRemoteBonusAndFill(id, userInfo);
        return Boolean.TRUE;
    }, executor);

    CompletableFuture growthFuture = CompletableFuture.supplyAsync(() -> {
        getRemoteGrowthAndFill(id, userInfo);
        return Boolean.TRUE;
    }, executor);
    CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join();

    userFuture.get();
    bonusFuture.get();
    growthFuture.get();

    return userInfo;
}

As a warm reminder, don’t forget to use the thread pool in these two ways. In the example, I used executor, which represents a custom thread pool. In order to prevent the problem of too many threads in high concurrency scenarios.

also,Fork/joinFrameworks also provide the ability to perform tasks and return results.

2. Data loss

Let’s take the registered user interface as an example. This interface mainly includes functions such as writing user tables, assigning permissions, configuring user navigation pages, and sending notification messages.

Among them: the functions of writing user tables and assigning permissions need to be executed synchronously in a transaction. The remaining functions of configuring user navigation pages and sending notification messages are executed asynchronously using multiple threads.

On the surface it looks fine.

However, if the previous functions of writing the user table and assigning permissions are successful, the user registration interface will directly return success.

But what if the subsequent asynchronous configuration of the user navigation page or the function of sending a notification message fails?

As shown below:

The interface has clearly prompted the user to succeed, but as a result, some functions later failed in the multi-threaded asynchronous execution.

How to deal with this?

Yes, you can doretry on failure

But if it is retried for a certain number of times and still unsuccessful, what should I do with this request data? If no processing is done, is the data lost?

To prevent data loss, the following solutions can be used:

  1. Asynchronous processing using mq. After assigning permissions, send an mq message to the mq server, and then use multithreading in the mq consumer to configure the user navigation page and send notification messages. If the processing in the mq consumer fails, you can retry it yourself.
  2. Asynchronous processing using job. After assigning permissions, write a piece of data to the task table. Then there is a job that scans the table regularly, then configures the user navigation page and sends notification messages. If the job fails to process a piece of data, you can record a number of retries in the table, and then try again. But this scheme has a disadvantage, that is, the real-time performance may not be very high.

3. Sequence issues

If you use multithreading, you have to accept a very real problem, namelysequence problem

If the execution order of the previous code is: a,b,c, after changing to multi-threaded execution, the execution order of the code may become: a,c,b. (This is related to the cpu scheduling algorithm)

E.g:

public static void main(String[] args) {
    Thread thread1 = new Thread(() -> System.out.println("a"));
    Thread thread2 = new Thread(() -> System.out.println("b"));
    Thread thread3 = new Thread(() -> System.out.println("c"));

    thread1.start();
    thread2.start();
    thread3.start();
}

Results of the:

a
c
b

So, a question from the soul: how to ensure the order of threads?

That is, the order of thread startup is: a,b,c, and the order of execution is also: a,b,c.

As shown below:

3.1 join

ThreadCategoryjoinmethod, it will make the main thread wait for the child thread to finish running before continuing to run.

Columns such as:

public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new Thread(() -> System.out.println("a"));
    Thread thread2 = new Thread(() -> System.out.println("b"));
    Thread thread3 = new Thread(() -> System.out.println("c"));

    thread1.start();
    thread1.join();
    thread2.start();
    thread2.join();
    thread3.start();
}

The result of execution is always:

a
b
c

3.2 newSingleThreadExecutor

We can use the JDK that comes withExcutorsCategorynewSingleThreadExecutormethod, create asingle threadofThread Pool

E.g:

public static void main(String[] args)  {
    ExecutorService executorService = Executors.newSingleThreadExecutor();

    Thread thread1 = new Thread(() -> System.out.println("a"));
    Thread thread2 = new Thread(() -> System.out.println("b"));
    Thread thread3 = new Thread(() -> System.out.println("c"));

    executorService.submit(thread1);
    executorService.submit(thread2);
    executorService.submit(thread3);

    executorService.shutdown();
}

The result of execution is always:

a
b
c

useExcutorsCategorynewSingleThreadExecutorThe single-threaded thread pool created by the method usesLinkedBlockingQueueas a queue, and this queue isFIFO(First in, first out) Sort elements.

The order of adding to the queue is a,b,c, and the order of execution is also a,b,c.

3.3 CountDownLatch

CountDownLatchIs a synchronization tool class that allows one or more threads to wait until other threads have finished executing.

E.g:

public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch1 = new CountDownLatch(0);
        CountDownLatch latch2 = new CountDownLatch(1);
        CountDownLatch latch3 = new CountDownLatch(1);

        Thread thread1 = new Thread(new TestRunnable(latch1, latch2, "a"));
        Thread thread2 = new Thread(new TestRunnable(latch2, latch3, "b"));
        Thread thread3 = new Thread(new TestRunnable(latch3, latch3, "c"));

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class TestRunnable implements Runnable {

    private CountDownLatch latch1;
    private CountDownLatch latch2;
    private String message;

    TestRunnable(CountDownLatch latch1, CountDownLatch latch2, String message) {
        this.latch1 = latch1;
        this.latch2 = latch2;
        this.message = message;
    }

    @Override
    public void run() {
        try {
            latch1.await();
            System.out.println(message);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        latch2.countDown();
    }
}

The result of execution is always:

a
b
c

Additionally, useCompletableFutureofthenRunmethod, but also the execution order of multiple threads, which will not be introduced one by one here.

4. Thread safety issues

Since threads are used, there will also be thread safety issues that accompany them.

If there is such a requirement now: execute the query method with multiple threads, and then add the execution result to a list collection.

code show as below:

List list = Lists.newArrayList();
 dataList.stream()
     .map(data -> CompletableFuture
          .supplyAsync(() -> query(list, data), asyncExecutor)
         ));
CompletableFuture.allOf(futureArray).join();

useCompletableFutureAsynchronous multi-threaded execution of the query method:

public void query(List list, UserEntity condition) {
   User user = queryByCondition(condition);
   if(Objects.isNull(user)) {
      return;
   }
   list.add(user);
   UserExtend userExtend = queryByOther(condition);
   if(Objects.nonNull(userExtend)) {
      user.setExtend(userExtend.getInfo());
   }
}

In the query method, add the obtained query results to the list collection.

As a result, there will be thread safety problems in the list, and sometimes there will be less data, of course, it is not necessarily necessary.

This is becauseArrayListYesNot thread safeyes, not usedsynchronizedetc. keyword modification.

How to solve this problem?

Answer: useCopyOnWriteArrayListcollection, instead of ordinaryArrayListCollection, CopyOnWriteArrayList is a thread-safe opportunity.

A small change in one line is all it takes:

List list Lists.newCopyOnWriteArrayList();

As a warm reminder, the way to create a collection here uses google’s collect package.

5. ThreadLocal gets data exception

we all knowJDKIn order to solve the problem of thread safety, a new idea of ​​​​using space for time is provided:ThreadLocal

Its core idea is: shared variables in eachthreadhave onecopy, each thread operates its own copy and has no effect on other threads.

E.g:

@Service
public class ThreadLocalService {
    private static final ThreadLocal threadLocal = new ThreadLocal<>();

    public void add() {
        threadLocal.set(1);
        doSamething();
        Integer integer = threadLocal.get();
    }
}

ThreadLocal can indeed obtain correct data in ordinary threads.

But in real business scenarios, it is generally rarely used.separate thread, most of them useThread Pool

So, how to get it in the thread poolThreadLocalWhat about the data generated by the object?

If you use ordinary ThreadLocal directly, you will obviously not be able to get the correct data.

let’s tryInheritableThreadLocal, the specific code is as follows:

private static void fun1() {
    InheritableThreadLocal threadLocal = new InheritableThreadLocal<>();
    threadLocal.set(6);
    System.out.println("The parent thread gets data: " + threadLocal.get());

    ExecutorService executorService = Executors.newSingleThreadExecutor();

    threadLocal.set(6);
    executorService.submit(() -> {
        System.out.println("Get data from thread pool for the first time: " + threadLocal.get());
    });

    threadLocal.set(7);
    executorService.submit(() -> {
        System.out.println("Get data from the thread pool for the second time: " + threadLocal.get());
    });
}

Results of the:

Parent thread gets data: 6
The first time to get data from the thread pool: 6
The second time to get data from the thread pool: 6

Since the singleton thread pool is used in this example, the fixed number of threads is 1.

When submitting a task for the first time, the thread pool will automatically create a thread. Because InheritableThreadLocal is used, when a thread is created, its init method is called to copy the inheritableThreadLocals data in the parent thread to the child thread. So we see that setting the data to 6 in the main thread gets the correct data 6 from the thread pool for the first time.

After that, the data is changed to 7 in the main thread, but it is still 6 when the data is obtained from the thread pool for the second time.

Because when submitting the task for the second time, there is already a thread in the thread pool, so it is directly reused, and the thread will not be recreated. Therefore, the thread’s init method will not be called again, so the latest data 7 is not obtained the second time, and the old data 6 is still obtained.

So, how to do this?

Answer: useTransmittableThreadLocal, it is not a class that comes with the JDK, but a class in the Alibaba open source jar package.

The jar package can be imported through the following pom file:

com.alibaba
   transmittable-thread-local
   2.11.0
   compile

The code is adjusted as follows:

private static void fun2() throws Exception {
    TransmittableThreadLocal threadLocal = new TransmittableThreadLocal<>();
    threadLocal.set(6);
    System.out.println("The parent thread gets data: " + threadLocal.get());

    ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

    threadLocal.set(6);
    ttlExecutorService.submit(() -> {
        System.out.println("Get data from thread pool for the first time: " + threadLocal.get());
    });

    threadLocal.set(7);
    ttlExecutorService.submit(() -> {
        System.out.println("Get data from the thread pool for the second time: " + threadLocal.get());
    });

}

Results of the:

Parent thread gets data: 6
The first time to get data from the thread pool: 6
The second time to get data from the thread pool: 7

We can see that after using TransmittableThreadLocal, the latest data 7 can be correctly obtained from the thread for the second time.

nice。

If you look closely at this example, you may find that in addition to usingTransmittableThreadLocalIn addition to the class, also usedTtlExecutors.getTtlExecutorServicemethod to createExecutorServiceobject.

This is a very important place, without this step,TransmittableThreadLocalSharing data in a thread pool will not work.

createExecutorServiceobject, the underlying submit method willTtlRunnableorTtlCallableobject.

Taking the TtlRunnable class as an example, it implementsRunnableinterface, and also implements its run method:

public void run() {
    Map, Object> copied = (Map)this.copiedRef.get();
    if (copied != null && (!this.releaseTtlValueReferenceAfterRun || this.copiedRef.compareAndSet(copied, (Object)null))) {
        Map backup = TransmittableThreadLocal.backupAndSetToCopied(copied);

        try {
            this.runnable.run();
        } finally {
            TransmittableThreadLocal.restoreBackup(backup);
        }
    } else {
        throw new IllegalStateException("TTL value reference is released after run!");
    }
}

The main logic of this code is as follows:

  1. Make a backup of the ThreadLocal at that time, and then copy the ThreadLocal of the parent class.
  2. Execute the real run method to get the latest ThreadLocal data of the parent class.
  3. From the backed up data, restore the ThreadLocal data at that time.

If you want to know more about how ThreadLocal works, you can check out my other article “ThreadLocal kills 11 consecutive questions

6. OOM problem

As we all know, using multithreading can improve the efficiency of code execution, but it is not absolute.

For some time-consuming operations, using multithreading can indeed improve the efficiency of code execution.

But the more threads are created, the better. If too many threads are created, it may also causeOOMabnormal.

E.g:

Caused by: 
java.lang.OutOfMemoryError: unable to create new native thread

existJVMCreate a thread in the default need to occupy1Mmemory space.

If too many threads are created, it will inevitably lead to insufficient memory space, resulting in an OOM exception.

In addition to this, if you use a thread pool, especially a fixed size thread pool, that is, useExecutors.newFixedThreadPoolmethod to create a thread pool.

the thread poolnumber of core threadsandmaximum number of threadsis the same, it is a fixed value, and the queue that stores the message isLinkedBlockingQueue

The maximum capacity of the queue isInteger.MAX_VALUE, that is to say, if you use a fixed-size thread pool and store too many tasks, it may also cause an OOM exception.

java.lang.OutOfMemeryError:Java heap space

7. CPU usage soars

I don’t know if you have done the excel data import function, you need to import a batch of excel data into the system.

Each piece of data has some business logic. If all data is imported in a single thread, the import efficiency will be very low.

So it was changed to multi-threaded import.

If there is a lot of data in excel, it is very likely that there will be a problem of high CPU usage.

We all know that if the code has an infinite loop, the CPU usage will be much higher. Because the code keeps looping in a thread and cannot switch to other threads, the cpu is always occupied, so the cpu usage will remain high.

While multi-threading imports a large amount of data, although there is no infinite loop code, because multiple threads have been processing data non-stop, it takes up a long time on the CPU.

There is also a problem of high cpu usage.

So, how to solve this problem?

Answer: useThread.sleepTake a nap.

After processing a piece of data in the thread, sleep for 10 milliseconds.

Of course, there are many reasons for the high CPU usage. Multi-threaded data processing and infinite loop are only two of them. There are also frequent GC, regular matching, frequent serialization and deserialization, etc.

Later, I will write a special article on the reasons for the high CPU usage. Interested friends can pay attention to my follow-up articles.

8. Transactional Issues

In actual project development, there are still quite a lot of usage scenarios for multithreading. Will there be a problem if spring transactions are used in a multi-threaded scenario?

E.g:

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {

    @Transactional
    public void doOtherThing() {
        System.out.println("Save role table data");
    }
}

From the above example, we can seetransaction methodIn add, the transaction method doOtherThing is called, buttransaction methoddoOtherThing is in anotherthreadcalled in.

This will cause the two methods to not be in the same thread, and the obtainedDatabase linkagenot the same, thus two differentaffairs. If you want to throw an exception in the doOtherThing method, it is impossible for the add method to roll back.

If you have read the source code of spring transactions, you may know that spring transactions are implemented through database connections. A map is saved in the current thread, and the key isdata source, value isDatabase linkage

private static final ThreadLocal> resources =

  new NamedThreadLocal<>("Transactional resources");

what we saidthe same transaction, actually meanssame database connection, only if you have the same database connection can you simultaneouslysubmitandrollback. if in differentthread, gotDatabase linkageIt’s definitely not the same, so it’s a different transaction.

So don’t start another thread in the transaction to process the business logic, which will cause the transaction to fail.

9. Cause the service to hang

Using multithreading will cause the service to hang, which is not an alarmist, but a fact.

Suppose there is such a business scenario: the order query interface needs to be called in the consumer of mq, and after the data is found, it is written into the business table.

It was supposed to be no problem.

Suddenly one day, the mq producer ran a batch data processing job, which caused a large number of messages to accumulate on the mq server.

At this time, the processing speed of mq consumers is far behind the production speed of mq messages, resulting in a large amount of message accumulation, which has a great impact on users.

To solve this problem, mq consumer is changed toMultithreadingprocessing, use directlyThread Pool,andmaximum number of threadsConfigured to 20.

After this adjustment, the message accumulation problem has indeed been solved.

But it brought another more serious problem: the concurrency of the order query interface was too large, and the pressure was a bit unbearable, causing the services of some nodes to directly hang up.


In order to solve the problem, service nodes have to be temporarily added.

When using multithreading in mq consumers, when calling an interface, be sure to evaluate the maximum amount of access the interface can withstand to prevent the service from hanging due to excessive pressure.

One last word (please pay attention, don’t prostitute me for nothing)

If this article is helpful or enlightening to you, please scan the QR code and pay attention. Your support is the biggest motivation for me to keep on writing.
Ask for one-click three links: like, forward, and watch.
Follow the official account: [Su San said technology], reply in the official account: interviews, code artifacts, development manuals, time management have great fan benefits, and reply: join the group, you can communicate and learn with many seniors of BAT manufacturers .

Recommended Today

Laravel Tutorial – Practical Jam Community Open Source E-commerce API System

Important notice: The open source e-commerce version source code of Laravel + applet has been pulled on github, welcome to submit issue and star 🙂Open source e-commerce server:Laravel API source code Open source e-commerce client:applet source code Introduction to Jam Community IYOYO company was founded in Shanghai in 2011. After 8 years of industry accumulation, […]