Love life, love coding, wechat search [Architecture Technology column] pay attention to this place like sharing.

This article has been included in the architecture and technology column, with various videos, materials and technical articles.

When should you use multithreading?

Today, I saw a problem, and I suddenly felt inspired and wanted to talk about this topic.

I don’t know if you’ve thought about this, but when should I use multithreading? Does using multiple threads necessarily improve system performance?

Whether or not you should use multithreading depends largely on the type of application.

  • For computation-intensive (such as pure math) machines that are cpu-constrained, only multi-CPU (or multi-core) machines can benefit from more threads. Multi-threading on a single CPU does not bring any performance gains, but may degrade performance due to additional overhead such as thread switching

  • I/O intensive, when my application has to wait for slow resources (such as network connections or database connections to return data), then multi-threading makes full use of the system’s CPU. When one thread blocks and hangs, another thread can continue to use CPU resources.

  • In fact, multithreading does not increase the processing power of the CPU, but makes better use of CPU resources.

Since multiple threads of the same process share the same piece of memory resources, it will inevitably increase its complexity while bringing convenience, and how to ensure the consistency of multi-thread access to data. And multithreading is one of those areas of programming where it’s easy to stumble. It is also difficult to locate multithreaded programming problems. In general, good multithreading is written, and relying on testing to find multithreading problems is extremely unreliable. SO, study hard.

Keywords: volatile, synchronized, wait, notify. With these keywords in mind, you can write multithreaded code.

Two, when do you need to lock?

In multi-threaded situations, the most important thing is to ensure the consistency of data, and the consistency of data protection, it needs to use the lock.

In fact, one of the things we should be clear about in a multi-threaded scenario is, what do we need to protect? Not all data needs to be locked. Only data that is shared by multiple threads needs to be locked.

The essence of locking is to ensure that only one thread is accessing the shared data at a time, so that the shared data can be effectively protected.

For example, let’s say we want to construct a unidirectional list that is safe in multiple threads:

If there are two threads working on the list, one writer inserts a new element 7, and the other reader iterates through the list data, the following order of execution is possible if no locks are used:

steps Write a thread Read the thread
0 Change the next pointer to element 7 on element 2 . .
1 . . Iterate through the list and find that next is null at 7
2 Change the next pointer to element 7 to 3 . .

It is clear from the above example that working with the list in multiple threads can cause the reader thread to read incomplete data, only from the head of the list to the position of element 7. It can be seen that the multi-threaded protection without any protection measures will inevitably lead to data chaos. To avoid data consistency issues, we need to place the code that operates on the queue in a synchronized block (the object of the lock is the list instance) to ensure that only one thread can access the list at a time.

How to lock?

To keep things simple, we use synchronized (it’s recommended to use this keyword if you don’t have a special requirement, it’s really fast in new JDK versions), and remember that synchronized locks are the object headers.

Briefly speaking, there are mainly the following usages:

  • Synchronized is placed on a method and locks the object instance of the current synchronized method

  • Synchronized On static methods, the class object of synchronized is locked.

  • Synchronized (this) Synchronized (this) In a code block, it locks the object inside the block’s parentheses, where this refers to the class instance object on which the method is invoked

Three, multithreading is easy to make mistakes

1. The lock range is too large

After the shared resource access is complete, subsequent code is not placed outside the synchronized synchronized code block. As a result, the current thread invalidly occupies the lock for a long time, while other threads competing for the lock have to wait, which greatly affects performance.

 public void test(a)
 {
 		synchronized(lock){ ... .// Accessing a shared resource. .// Perform other time-consuming operations that have nothing to do with shared resources}}Copy the code

This will cause the thread to hold the lock for a long time, which will cause other threads to wait.

1) In single-CPU scenarios, time-consuming operations that do not need to be synchronized are taken out of the synchronization block, which can improve performance in some cases but not in others.

  • CPU intensive code, no low CPU consumption code such as disk IO/ network IO. In this case, the CPU is executing 99% of the code. Therefore, there is no performance gain from shrinking synchronized blocks, and there is no performance loss from shrinking synchronized blocks.

  • IO – intensive code that executes code that does not consume CPU while the CPU is idle. Getting the CPU to work at this point will result in an overall performance improvement. So in this case, time-consuming operations that do not need to be synchronized can be moved out of the synchronized block.

2) In multi-CPU scenarios, taking time-consuming CPU operations out of the synchronization block always improves performance

  • CPU intensive code, no CPU consuming code fragments such as IO operations. Because there are currently multiple cpus, other cpus may also be idle. So shrinking the synchronized block also allows other threads to execute the code as quickly as possible, resulting in a performance boost.

  • IO – intensive code, because the PCU is currently idle, is bound to improve overall performance by keeping time-consuming operations out of the synchronized block.

Of course, narrowing the lock synchronization scope is beneficial to the system anyway, so the code above should be changed to:

 public void test(a)
 {
 		synchronized(lock){ ... .// Accessing a shared resource}... .// Perform other time-consuming operations that have nothing to do with shared resources
 }
Copy the code

To sum up, one important point is to keep only code that accesses shared resources in synchronized blocks to ensure fast in and out.

Deadlock issues

Deadlocks to know:

  • A deadlock, simply put, occurs when two or more threads are waiting for a lock held by each other at the same time. A deadlock causes the thread to be unable to continue execution and to be permanently suspended.

  • If a thread has a deadlock, a “Found one Java-level deadlock” can be clearly seen in the thread stack, and the thread stack will also give the deadlock analysis result.

  • Deadlock is a problem that can cause a system crash if it occurs on a critical system, and the only way to quickly restore the system is to keep the thread stack restarted and then restore it as soon as possible.

  • Deadlock issues are sometimes difficult to detect immediately, and many times it’s up to luck and the test cases you prepare.

  • The only way to avoid problems like deadlocks is to change the code. But a reliable system is designed, rather than by changing the BUG to change out, when this kind of problem needs to be analyzed from the perspective of system design.

  • One would think that a deadlock would cause the CPU to be 100%, but it’s not true. Depending on what type of lock is used, such as a synchronized deadlock, it will not cause cpu100%, only suspend the thread. But if it’s a spin lock that’s probably going to consume CPU.

3. The problem of sharing a lock

This is when multiple shared variables can share a lock, especially with synchronized at the method level, resulting in artificial lock contention.

Here are some rookie mistakes:

1 public class MyTest
2 {
3 Object shared;
4 synchronized void fun1(a) {... }// Access the shared variable
5 synchronized void fun2(a) {... }// Access the shared variable
6 synchronized void fun3(a) {... }// Do not access the shared variable shared
7 synchronized void fun4(a) {... }// Do not access the shared variable shared
8 synchronized void fun5(a) {... }// Do not access the shared variable shared
9 }
Copy the code

Synchronized is added to every method in the above code, which clearly violates the principle of protecting any lock.

Three, the number of threads we generally set how much is more reasonable?

In fact, we all know that multithreading can improve the performance and throughput of the system in most situations, but how many threads is a reasonable system?

In general, having too many threads or too few threads is not a good idea. Too many threads can lead to overhead and sometimes lower system performance due to frequent thread switching. Less CPU resources will not be fully utilized, and the performance will not reach the bottleneck.

So, the system exactly how many threads appropriate, is to see whether the system threads can make full use of the CPU. In fact, most of the time, it does not consume CPU, such as disk I/O and network I/O.

Disk I/O and network I/O are very slow compared to the SPEED of the CPU, which is idle during the time of executing the I/O. If other threads can use this idle CPU, it can achieve the purpose of system performance and throughput.

In fact, we also mentioned above, namely, two kinds of computing properties:

CPU intensive: Because each CPU is a high computational load, setting too many threads can lead to unnecessary context switches. This “extra” thread ensures that the CPU clock cycles are not wasted, even when computationally intensive threads are occasionally paused due to missing pages or other reasons.

IO intensive: The CPU will be idle due to a large number of IO operations, so we can set more threads. So, the number of threads = number of CPU cores * (1+ I/O time /CPU time) is ok.

Love life, love coding, wechat search [Architecture Technology column] pay attention to this place like sharing.

This article has been included in the architecture and technology column, with various videos, materials and technical articles.