This series of concurrent articles is based on reading the Art of Concurrent Programming in Java, which you can purchase for further study.

Concurrent programming is designed to make a program run faster, but starting more threads does not necessarily result in maximum concurrent execution, and sometimes concurrency is even slower than serial execution. There are many challenges when using concurrent programming.

Context switch

Single-core processors can also support multi-threaded code execution. The CPU implements this mechanism by allocating time slices. A time slice is the amount of time that the CPU allocates to each thread, and because the time slice is so short, the CPU keeps switching threads to make it feel like multiple threads are executing simultaneously.

Before each time slice switch, the state of the previous task is saved, so that the state of the task can be loaded again when the task is switched back to next time. The save to load process is a context switch.

Such switching will affect the efficiency of multithreading. Imagine we read an English book, if we don’t know the word, we will look it up in the dictionary, but before looking it up, we have to remember that we have seen the page, so that we can continue to read at the same position when we find the word. While this will ensure the consistency of reading, the speed of reading will inevitably be affected.

Multi-threaded concurrent execution is not necessarily faster than serial execution. It is found that the speed of concurrent operation is not as fast as that of serial operation until the number of cycles is reached. This is because of the overhead of thread creation and context switching.

There are tools to measure context consumption:

  • The duration of context switching can be measured using Lmbench3
  • You can measure the number of context switches using vmstat

So how do you reduce context switching?

  • Lockless concurrent programming. Context switches occur when multiple threads compete for locks, so you can use a one-line approach to avoid using locks. For example, the ID of the data is segmented according to the Hash algorithm. Different threads process different segments of the data.
  • CAS algorithm. Java’s Atomic package uses the CAS algorithm to update data without locking
  • Use the minimum number of threads: Avoid creating unnecessary threads. Creating too many extra threads will result in a large number of threads waiting.
  • Coroutine: Performs scheduling of multiple tasks in a single thread and maintains switching between tasks in a single thread.

A deadlock

Locks are a very useful tool and easy to use, but they can cause deadlocks and make the system unusable. Deadlock problems can occur in complex scenarios, such as after thread T1 has acquired the lock, because some exception does not release the lock (such as an infinite loop). Or t1 could have acquired a database lock and released it with an exception.

Once a deadlock occurs, the business is aware that the service can no longer be provided. We can use the dump site to see which thread is in trouble and trace the code based on the log information.

A few common ways to avoid deadlocks:

  • Avoid one thread acquiring multiple locks at the same time
  • Avoid using multiple resources in a lock by one thread. Try to use only one resource per lock.
  • Try using the timing locklock.tryLock(timeout)Instead of using an internal locking mechanism.
  • The database lock must be unlocked in a database connection; otherwise, the unlock will fail.

Challenges of resource constraints

Resource constraints can be divided into computer hardware resources and software resources.

Hardware resource limitations include broadband speed, disk read/write speed, and CPU processing speed. If the broadband speed is only 2Mb/s, the download speed of a resource is 1Mb/s, but the system will not program 10Mb/s download speed even if 10 threads are started to download the resource.

Software resources are limited by the number of database connections and socket connections.

Due to resource constraints, sometimes concurrent tasks are not even as fast as serial execution due to insufficient resources. For example, when multi-threading is enabled, some resource requests are made, but the resources are not enough, which will lead to some overhead of resource scheduling and context switching.

For hardware resource constraints, cluster parallel execution can be used. For software resources, you can use resource pools to reuse the resources. In the case of resource limits, the concurrency of the program is adjusted for different resource limits. For example, if the SQL statement is executed very fast, but the number of threads is much larger than the number of database connections, some threads will block, waiting for the database connection.

conclusion

There are many challenges in concurrent programming. If the concurrent program is not written rigorously, problems will be difficult and time-consuming to locate and solve. Therefore, for Java developers, it is recommended to use the concurrent containers and utility classes provided by JDK and distributed packages to solve the concurrency problems. These classes have been fully tested and optimized, and the above problems can be solved.