Home > Java > javaTutorial > body text

Summary of 40 Java multithreading issues

巴扎黑
Release: 2017-04-30 10:10:02
Original
1114 people have browsed it

Foreword

There are 21 multi-threading articles written in the Java multi-threading category. The 21 articles contain a lot of content. Personally, I think that the more content and more complex knowledge you learn, the more you need to make a profound summary, so that you can remember it deeply and turn the knowledge into something. Become your own. This article mainly summarizes multi-threading issues, so 40 multi-threading issues are listed.

Some of these multi-threading problems come from major websites, and some come from my own thinking. There may be some questions on the Internet, there may be answers to some questions, and there may be some that all netizens have read, but the focus of writing this article is that all questions will be answered according to your own understanding, and you will not look at the answers on the Internet, so Maybe some of the questions are wrong, and I hope you can correct me.

Summary of 40 questions

1. What is the use of multi-threading?

A question that may seem nonsense to many people: As long as I can use multi-threading, what is the use of it? In my opinion, this answer is even more nonsense. The so-called "knowing how it is, knowing why it is", "knowing how to use it" is just "knowing how it is", "why you use it" is "knowing how it is", only to the extent of "knowing how it is, knowing why it is" can it be said to be "knowing how it is" A knowledge point can be used freely. OK, let me talk about my views on this issue:

(1) Take advantage of multi-core CPU

With the advancement of industry, today's notebooks, desktops and even commercial application servers are at least dual-core. 4-core, 8-core or even 16-core are not uncommon. If it is a single-threaded program, then on a dual-core CPU 50% is wasted, and 75% is wasted on a 4-core CPU. The so-called "multi-threading" on a single-core CPU is fake multi-threading. The processor will only process a piece of logic at the same time, but the switching between threads is faster, and it looks like multiple threads are running "simultaneously". Multi-threading on a multi-core CPU is the real multi-threading. It allows your multiple pieces of logic to work at the same time. Multi-threading can truly take advantage of the multi-core CPU and achieve the purpose of making full use of the CPU.

(2) Prevent blocking

From the perspective of program running efficiency, a single-core CPU will not only not take advantage of multi-threading, but will also reduce the overall efficiency of the program because running multi-threads on a single-core CPU will lead to thread context switching. But for single-core CPUs, we still need to use multi-threading to prevent blocking. Just imagine, if a single-core CPU uses a single thread, then as long as this thread is blocked, for example, reading certain data remotely, and the peer has not returned yet and has not set a timeout, then your entire program will be blocked before the data is returned. Stopped running. Multi-threading can prevent this problem. Multiple threads run at the same time. Even if the code execution of one thread is blocked in reading data, it will not affect the execution of other tasks.

(3) Convenient modeling

This is another less obvious advantage. Suppose there is a large task A, single-threaded programming, then there is a lot to consider, and it is troublesome to build the entire program model. But if you break down this large task A into several small tasks, task B, task C, and task D, establish program models respectively, and run these tasks separately through multi-threads, it will be much simpler.

2. How to create a thread

A relatively common question, generally there are two types:

(1) Inherit Thread class

(2) Implement Runnable interface

As for which one is better, it goes without saying that the latter is better, because the way of implementing interfaces is more flexible than the way of inheriting classes, and can also reduce the coupling between programs. Interface-oriented programming is also the core of the six principles of design patterns.

3. The difference between start() method and run() method

Only when the start() method is called will the multi-threading feature be shown, and the code in the run() methods of different threads will be executed alternately. If you just call the run() method, the code is still executed synchronously. You must wait for all the code in one thread's run() method to be executed before another thread can execute the code in its run() method.

4. The difference between Runnable interface and Callable interface

This is a bit of a deep question, and it also shows the breadth of knowledge a Java programmer can acquire.

The return value of the run() method in the Runnable interface is void, and what it does is purely to execute the code in the run() method; the call() method in the Callable interface has a return value and is a generic , can be used in conjunction with Future and FutureTask to obtain the results of asynchronous execution.

This is actually a very useful feature, because an important reason why multi-threading is more difficult and complex than single-threading is because multi-threading is full of unknowns. Has a certain thread been executed? How long has a thread been executed? When a thread executes, has the data we expect been assigned? There is no way to know, all we can do is wait for this multi-threaded task to complete. Callable+Future/FutureTask can obtain the results of multi-threaded operations, and can cancel the task of the thread if the waiting time is too long and the required data is not obtained, which is really very useful.

5. The difference between CyclicBarrier and CountDownLatch

Two classes that look a bit similar, both under java.util.concurrent, can be used to indicate that the code runs to a certain point. The difference between the two is:

(1) After a thread of CyclicBarrier runs to a certain point, the thread stops running. Until all threads reach this point, all threads re-run; CountDownLatch does not, a thread runs to a certain point. After that, just give a certain value -1, and the thread continues to run

(2) CyclicBarrier can only evoke one task, and CountDownLatch can evoke multiple tasks

(3) CyclicBarrier can be reused, but CountDownLatch cannot be reused. If the count value is 0, the CountDownLatch cannot be reused

6. The role of volatile keyword

A very important issue that every Java programmer who learns and applies multi-threading must master. The prerequisite for understanding the role of the volatile keyword is to understand the Java memory model. I will not talk about the Java memory model here. You can refer to point 31. The volatile keyword has two main functions:

(1) Multi-threading mainly revolves around the two characteristics of visibility and atomicity. Variables modified with the volatile keyword ensure their visibility between multi-threads, that is, every time a volatile variable is read, it must be the latest The data

(2) The underlying execution of the code is not as simple as the high-level language we see - Java program. Its execution is Java code-->bytecode-->executes the corresponding C/C++ according to the bytecode. Code-->C/C++ code is compiled into assembly language-->interacts with hardware circuits. In reality, in order to obtain better performance, the JVM may reorder instructions, and some unexpected problems may occur under multi-threading. The problem. Using volatile will reorder the prohibition semantics, which of course also reduces code execution efficiency to a certain extent

From a practical perspective, an important role of volatile is to combine with CAS to ensure atomicity. For details, please refer to the classes under the java.util.concurrent.atomic package, such as AtomicInteger.

7. What is thread safety

This is another theoretical question, and there are many different answers. I will give you the one that I think explains it best: if your code is executed in multiple threads and executed in a single thread, you will always get the same result. , then your code is thread-safe.

Something worth mentioning about this question is that there are several levels of thread safety:

(1) Immutable

Classes like String, Integer, and Long are all final types. No thread can change their values ​​unless they create a new one. Therefore, these immutable objects can be directly used in a multi-threaded environment without any synchronization means. use

(2) Absolute thread safety

Regardless of the runtime environment, no additional synchronization measures are required by the caller. To achieve this, you usually need to pay a lot of extra costs. In fact, most of the classes in Java that are marked as thread-safe are not thread-safe. However, there are also absolutely thread-safe classes in Java, such as CopyOnWriteArrayList and CopyOnWriteArraySet.

(3) Relative thread safety

Relative thread safety is what we usually call thread safety. Like Vector, the add and remove methods are atomic operations and will not be interrupted, but it is limited to this. If a thread is traversing a Vector , if there is a thread adding this Vector at the same time, ConcurrentModificationException will occur in 99% of cases, which is the fail-fast mechanism.

(4) Thread non-safety

There is nothing to say about this. ArrayList, LinkedList, HashMap, etc. are all thread-non-safe classes

8. How to obtain the thread dump file in Java

For problems such as infinite loops, deadlocks, blocking, and slow page opening, thread dump is the best way to solve the problem. The so-called thread dump is the thread stack. There are two steps to obtain the thread stack:

(1) To get the pid of the thread, you can use the jps command. In the Linux environment, you can also use ps -ef | grep java

(2) To print the thread stack, you can use the jstack pid command. In the Linux environment, you can also use kill -3 pid

In addition, the Thread class provides a getStackTrace() method that can also be used to obtain the thread stack. This is an instance method, so this method is bound to a specific thread instance. Each time you get it, you get the stack currently running by a specific thread,

9. What happens if a thread encounters a runtime exception

If this exception is not caught, the thread stops executing. Another important point is: if this thread holds a monitor for a certain object, then the object monitor will be released immediately

10. How to share data between two threads

Just share objects between threads, and then awaken and wait through wait/notify/notifyAll, await/signal/signalAll. For example, the blocking queue BlockingQueue is designed for sharing data between threads

11. What is the difference between sleep method and wait method?

This question is often asked. Both the sleep method and the wait method can be used to give up the CPU for a certain period of time. The difference is that if the thread holds the monitor of an object, the sleep method will not give up the monitor of this object, and the wait method will give up the monitor. Object monitor

12. What is the role of the producer-consumer model

This question is very theoretical, but very important:

(1) Improving the operating efficiency of the entire system by balancing the production capacity of producers and the consumption capacity of consumers. This is the most important role of the producer-consumer model

(2) Decoupling, which is an incidental function of the producer-consumer model. Decoupling means that there are fewer connections between producers and consumers. The fewer connections, the more they can develop independently without mutual constraints

13. What is the use of ThreadLocal

To put it simply, ThreadLocal is a method of exchanging space for time. Each Thread maintains a ThreadLocal.ThreadLocalMap implemented by the open address method, which isolates the data and does not share the data. Naturally, there are no thread safety issues.

14. Why wait() method and notify()/notifyAll() method are called in synchronized block

This is mandatory by the JDK. The wait() method and notify()/notifyAll() method must first obtain the object's lock before calling it

15. What is the difference between the wait() method and the notify()/notifyAll() method when giving up the object monitor?

The difference between the wait() method and the notify()/notifyAll() method when giving up the object monitor is that the wait() method releases the object monitor immediately, while the notify()/notifyAll() method waits for the remaining code of the thread to be executed. will give up the object monitor.

16. Why use thread pool

Avoid frequent creation and destruction of threads to achieve reuse of thread objects. In addition, using the thread pool can also flexibly control the number of concurrency according to the project.

17. How to detect whether a thread holds an object monitor

I also saw a multi-threading interview question on the Internet and learned that there is a way to determine whether a thread holds an object monitor: the Thread class provides a holdsLock(Object obj) method, if and only if the monitor of object obj is held by a certain thread. It will return true only when it is held by a thread. Note that this is a static method, which means that "a certain thread" refers to the current thread.

18. The difference between synchronized and ReentrantLock

synchronized is the same keyword as if, else, for, and while, and ReentrantLock is a class. This is the essential difference between the two. Since ReentrantLock is a class, it provides more and more flexible features than synchronized. It can be inherited, can have methods, and can have various class variables. The scalability of ReentrantLock compared to synchronized is reflected in several points:

(1) ReentrantLock can set the waiting time for acquiring the lock, thus avoiding deadlock

(2) ReentrantLock can obtain information about various locks

(3) ReentrantLock can flexibly implement multiple notifications

In addition, the locking mechanisms of the two are actually different. The bottom layer of ReentrantLock calls Unsafe's park method to lock, and synchronized should operate on the mark word in the object header. I am not sure about this.

19. What is the concurrency of ConcurrentHashMap

The concurrency of ConcurrentHashMap is the size of the segment. The default is 16, which means that up to 16 threads can operate ConcurrentHashMap at the same time. This is also the biggest advantage of ConcurrentHashMap over Hashtable. In any case, Hashtable can have two threads at the same time to obtain the contents of Hashtable. Data?

20. What is ReadWriteLock

First of all, let’s be clear, it’s not that ReentrantLock is bad, it’s just that ReentrantLock has limitations in some cases. If ReentrantLock is used, it may be to prevent data inconsistency caused by thread A writing data and thread B reading data. However, if thread C is reading data and thread D is also reading data, reading the data will not change the data. There is no need. Lock, but it is still locked, which reduces the performance of the program.

Because of this, the read-write lock ReadWriteLock was born. ReadWriteLock is a read-write lock interface. ReentrantReadWriteLock is a specific implementation of the ReadWriteLock interface, which realizes the separation of reading and writing. The read lock is shared and the write lock is exclusive. There is no mutual exclusion between reading and reading. Reading and writing, Writing and reading, writing and writing are mutually exclusive, which improves the performance of reading and writing.

21. What is FutureTask

In fact, as mentioned before, FutureTask represents an asynchronous operation task. A specific implementation class of Callable can be passed into FutureTask, and operations such as waiting to obtain the results of this asynchronous operation task, determining whether it has been completed, and canceling the task can be performed. Of course, since FutureTask is also an implementation class of the Runnable interface, FutureTask can also be placed in the thread pool.

22. How to find which thread uses the CPU the longest in a Linux environment

This is a more practical question, which I think is quite meaningful. You can do this:

(1) Get the pid of the project, jps or ps -ef | grep java, this has been mentioned before

(2) top -H -p pid, the order cannot be changed

This will print out the current project and the percentage of CPU time taken by each thread. Note that what is typed here is LWP, which is the thread number of the native thread of the operating system. My laptop has not deployed a Java project in the Linux environment, so there is no way to take screenshots for demonstration. Netizens, if your company uses the Linux environment to deploy projects, you can try it. one time.

Using "top -H -p pid" + "jps pid" you can easily find the thread stack of a thread that occupies a high amount of CPU, thereby locating the cause of the high CPU occupancy. This is usually due to improper code operations that lead to an infinite loop.

One last thing to mention, the LWP printed by "top -H -p pid" is in decimal, and the local thread number printed by "jps pid" is in hexadecimal. By converting it, you can locate the thread that takes up a lot of CPU. The current thread is stacked.

23. Java programming to write a program that will cause deadlock

The first time I saw this topic, I thought it was a very good question. Many people know what a deadlock is: thread A and thread B wait for each other's lock, causing the program to continue in an infinite loop. Of course, it’s limited to this. If you ask how to write a deadlock program, you won’t know. To put it bluntly, this situation means that you don’t understand what deadlock is. Just understand a theory and that’s it. In practice, you encounter deadlock problems. It's basically invisible.

To truly understand what deadlock is, this problem is actually not difficult, just a few steps:

(1) The two threads hold two Object objects respectively: lock1 and lock2. These two locks serve as locks for synchronized code blocks;

(2) The synchronization code block in the run() method of thread 1 first acquires the object lock of lock1, Thread.sleep(xxx). The time does not need to be too much, 50 milliseconds is almost enough, and then acquires the object lock of lock2. This is mainly done to prevent thread 1 from continuously acquiring the object locks of lock1 and lock2 objects

(3) Thread 2's run) (The synchronization code block in the method first acquires the object lock of lock2, and then acquires the object lock of lock1. Of course, at this time, the object lock of lock1 has been locked by thread 1, and thread 2 must wait for the thread. 1 Release the object lock of lock1

In this way, after thread 1 "sleeps", thread 2 has acquired the object lock of lock2. Thread 1 attempts to acquire the object lock of lock2 and is blocked. At this time, a deadlock is formed. I won’t write the code because it takes up a lot of space. Java Multithreading 7: Deadlock is included in this article, which is the code implementation of the above steps.

24. How to wake up a blocked thread

If the thread is blocked due to calling the wait(), sleep() or join() method, you can interrupt the thread and wake it up by throwing InterruptedException; if the thread encounters IO blocking, there is nothing you can do because IO is an operating system Implemented, Java code has no way to directly contact the operating system.

25. How does immutable objects help multi-threading?

As mentioned before, immutable objects ensure the memory visibility of objects. Reading immutable objects does not require additional synchronization methods, which improves code execution efficiency.

26. What is multi-threaded context switching

Multi-threaded context switching refers to the process of switching CPU control from one thread that is already running to another thread that is ready and waiting to obtain CPU execution rights.

27. What will happen if the thread pool queue is full when you submit a task

If you are using LinkedBlockingQueue, which is an unbounded queue, it doesn't matter. Continue to add tasks to the blocking queue and wait for execution, because LinkedBlockingQueue can be almost considered an infinite queue and can store tasks infinitely; if you are using a bounded queue, for example In the case of ArrayBlockingQueue, tasks will first be added to the ArrayBlockingQueue. If the ArrayBlockingQueue is full, the rejection policy RejectedExecutionHandler will be used to process the full tasks. The default is AbortPolicy.

28. What is the thread scheduling algorithm used in Java

Preemptive. After a thread uses up the CPU, the operating system will calculate a total priority based on thread priority, thread hunger and other data and allocate the next time slice to a certain thread for execution.

29. What is the function of Thread.sleep(0)

This question is related to the question above, so I connected them together. Since Java uses a preemptive thread scheduling algorithm, it may happen that a certain thread often obtains CPU control. In order to allow some threads with lower priority to obtain CPU control, you can use Thread.sleep( 0) Manually trigger an operation of the operating system to allocate time slices, which is also an operation to balance CPU control.

30. What is spin

A lot of synchronized code is just very simple code, and the execution time is very fast. Locking all waiting threads at this time may not be a worthwhile operation, because thread blocking involves switching between user mode and kernel mode. Since the code in synchronized executes very fast, it is better to let the thread waiting for the lock not to be blocked, but to do a busy loop at the boundary of synchronized. This is spin. If you do multiple busy loops and find that the lock has not been obtained, blocking again may be a better strategy.

31. What is Java memory model

The Java memory model defines a specification for multi-threaded access to Java memory. A complete explanation of the Java memory model cannot be explained clearly in a few sentences. Let me briefly summarize some parts of the Java memory model:

(1) The Java memory model divides memory into main memory and working memory. The state of the class, that is, the variables shared between classes, is stored in the main memory. Every time the Java thread uses these variables in the main memory, it will read the variables in the main memory once and let these memories exist. There is a copy in your own working memory. When running your own thread code, when you use these variables, you operate the copy in your own working memory. After the thread code is executed, the latest value will be updated to the main memory

(2) Several atomic operations are defined for operating variables in main memory and working memory

(3) Define the rules for using volatile variables

(4) happens-before, that is, the principle of occurrence before, defines some rules that operation A must occur before operation B. For example, in the same thread, the code in front of the control flow must occur before the code behind the control flow, a release lock The unlock action must precede the subsequent lock actions for the same lock, etc. As long as these rules are met, no additional synchronization measures are required. If a piece of code does not meet all the happens-before rules, then this code It must be thread-unsafe

32. What is CAS

CAS stands for Compare and Swap, which means compare and replace. Suppose there are three operands: memory value V, old expected value A, and value to be modified B. If and only if the expected value A and the memory value V are the same, the memory value will be modified to B and true will be returned. Otherwise, what Do neither and return false. Of course, CAS must cooperate with volatile variables to ensure that the variable obtained each time is the latest value in the main memory. Otherwise, the old expected value A will always be an unchanging value A for a certain thread. As long as If a CAS operation fails, it will never succeed.

33. What are optimistic locks and pessimistic locks

(1) Optimistic lock: Just like its name, it is optimistic about the thread safety issues caused by concurrent operations. Optimistic lock believes that competition will not always occur, so it does not need to hold the lock, and will compare and replace these two As an atomic operation, this action attempts to modify the variables in the memory. If it fails, it means a conflict occurs, and there should be corresponding retry logic.

(2) Pessimistic lock: Like its name, it is pessimistic about thread safety issues caused by concurrent operations. Pessimistic lock believes that competition will always occur, so every time a resource is operated on, it will hold an exclusive The lock is just like synchronized. No matter what happens, you can directly operate the resource after locking it.

34. What is AQS

Let’s briefly talk about AQS. The full name of AQS is AbstractQueuedSychronizer, which should be translated as abstract queue synchronizer.

If the basis of java.util.concurrent is CAS, then AQS is the core of the entire Java concurrency package. ReentrantLock, CountDownLatch, Semaphore, etc. all use it. AQS actually connects all Entries in the form of a bidirectional queue, such as ReentrantLock. All waiting threads are placed in an Entry and connected into a bidirectional queue. If the previous thread uses ReentrantLock, the bidirectional queue is actually the first Entry starts running.

AQS defines all operations on bidirectional queues, and only opens the tryLock and tryRelease methods to developers. Developers can rewrite the tryLock and tryRelease methods according to their own implementation to achieve their own concurrency functions.

35. Thread safety of singleton mode

It’s a commonplace question. The first thing to say is that the thread safety of the singleton mode means that an instance of a certain class will only be created once in a multi-threaded environment. There are many ways to write the singleton pattern. Let me summarize:

(1) How to write Hungry-style singleton mode: thread safety

(2) How to write lazy singleton mode: non-thread safe

(3) How to write double-check lock singleton mode: thread safety

36. What is the function of Semaphore?

Semaphore is a semaphore, its function is to limit the number of concurrency of a certain code block. Semaphore has a constructor that can pass in an int type integer n, which means that a certain piece of code can only be accessed by n threads at most. If n is exceeded, then please wait until a thread finishes executing this code block, and the next thread Re-enter. It can be seen from this that if the int type integer n=1 passed in the Semaphore constructor is equivalent to becoming a synchronized.

37. There is only one statement "return count" in Hashtable's size() method. Why does it need to be synchronized?

This was a confusion I had before. I wonder if anyone has thought about this problem. If there are multiple statements in a method, and they are all operating on the same class variable, then not locking in a multi-threaded environment will inevitably cause thread safety issues. This is easy to understand, but the size() method clearly has only one statement. , why should we lock it?

Regarding this issue, I gradually gained understanding while working and studying. There are two main reasons:

(1) Only one thread can execute the synchronized method of a fixed class at the same time, but the asynchronous method of the class can be accessed by multiple threads at the same time. Therefore, there is a problem. Maybe thread A is executing the put method of Hashtable to add data, and thread B can call the size() method normally to read the number of current elements in the Hashtable, and the value read may not be the latest. , maybe thread A has added the data, but thread B has already read size without adjusting size++, so the size read by thread B must be inaccurate. After adding synchronization to the size() method, it means that thread B can only call the size() method after thread A has completed calling the put method, thus ensuring thread safety

(2) The CPU executes code, not Java code. This is very important and must be remembered. Java code is eventually translated into assembly code for execution. Assembly code is the code that can actually interact with hardware circuits. Even if you see that there is only one line of Java code, or even if you see that the bytecode generated after compiling the Java code is only one line, it does not mean that for the bottom layer, this statement has only one operation. Assuming that the sentence "return count" is translated into three assembly statements for execution, it is entirely possible that the thread will be switched after the first sentence is executed.

38. Which thread is called the constructor method and static block of the thread class

This is a very tricky and cunning question. Please remember: the construction method and static block of the thread class are called by the thread where the new thread class is located, and the code in the run method is called by the thread itself.

If the above statement confuses you, let me give you an example. Suppose Thread1 is new in Thread2 and Thread2 is new in the main function. Then:

(1) The construction method and static block of Thread2 are called by the main thread, and the run() method of Thread2 is called by Thread2 itself

(2) The construction method and static block of Thread1 are called by Thread2, and the run() method of Thread1 is called by Thread1 itself

39. Which is a better choice, synchronization method or synchronization block?

Synchronized block, which means that the code outside the synchronized block is executed asynchronously, which improves the efficiency of the code than synchronizing the entire method. Please know one principle: the smaller the scope of synchronization, the better.

With this in mind, I would like to mention that although the smaller the scope of synchronization, the better, there is still an optimization method called lock coarsening in the Java virtual machine, which is to make the scope of synchronization larger. This is useful, for example, StringBuffer is a thread-safe class. Naturally, the most commonly used append() method is a synchronization method. When we write code, we will repeatedly append the string, which means repeated locking. -> Unlocking, which is bad for performance, because it means that the Java virtual machine has to repeatedly switch between kernel mode and user mode on this thread, so the Java virtual machine will process the code that calls multiple append methods. The lock coarsening operation extends the multiple append operations to the beginning and end of the append method and turns it into a large synchronization block. This reduces the number of locks and unlocks, and effectively improves the efficiency of code execution. efficiency.

40. How to use thread pool for businesses with high concurrency and short task execution time? How to use thread pool for businesses with low concurrency and long task execution time? How to use thread pool for businesses with high concurrency and long business execution time?

This is a question I saw on the concurrent programming Internet. I put this question at the last one. I hope everyone can see it and think about it, because this question is very good, very practical, and very professional. Regarding this issue, my personal opinion is:

(1) For businesses with high concurrency and short task execution time, the number of thread pool threads can be set to the number of CPU cores + 1 to reduce thread context switching

(2) Businesses with low concurrency and long task execution time should be distinguished:

a) If the business time is concentrated on IO operations for a long time, that is, IO-intensive tasks, because IO operations do not occupy the CPU, so do not let all the CPUs idle, you can increase the number of threads in the thread pool, so that the CPU Handle more business

b) If the business time is concentrated on computing operations for a long time, that is, computing-intensive tasks, there is no way to do this. It is the same as (1). The number of threads in the thread pool is set to be smaller to reduce thread context switching

(3) High concurrency and long business execution time. The key to solving this type of task lies not in the thread pool but in the design of the overall architecture. Seeing whether certain data in these businesses can be cached is the first step, and adding servers is the third. In the second step, as for the thread pool settings, please refer to (2) for the settings. Finally, the problem of long business execution time may also need to be analyzed to see if middleware can be used to split and decouple tasks.

The above is the detailed content of Summary of 40 Java multithreading issues. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!