Do you know all the things you need to pay attention to when using threads?

Do you know all the things you need to pay attention to when using threads?

[[344283]]

This article is reprinted from the WeChat public account "Huai Meng Zhuima", which can be followed through the following QR code. To reprint this article, please contact the Huai Meng Zhuima public account.

1. Synchronize access to shared data

question

Concurrent programs are more complex to design than single-threaded programs, and failures are harder to reproduce. However, multithreading is unavoidable, because it is an effective way to get the best performance from multi-core computers. When it comes to concurrency, if mutable data is involved, this is where we need to focus our thinking. When facing concurrent access to mutable data, what methods can ensure thread safety?

Answer

  • When an object is modified by one thread, it can prevent another thread from observing an inconsistent state inside the object;
  • Synchronization not only prevents a thread from seeing an object in an inconsistent state, but also ensures that each thread that enters a synchronized method or synchronized code block sees the effects of all previous modifications protected by the same lock.

1. Keyword synchronized: synchronized is a powerful tool to ensure thread safety. It can ensure that only one thread can execute a method and modify a variable data at the same time. However, it is not entirely correct to simply understand it as mutually exclusive. It has two main meanings:

In addition, the Java language specification guarantees that reading and writing a variable is atomic, unless the variable is a double or long, even without synchronization.

Considering such an example, the thread achieves the function of gracefully stopping the thread by polling the flag bit. The sample code is as follows:

  1. private static boolean stopRequested;
  2. private static synchronized void requestStop() {
  3. stopRequested = true ;
  4. }
  5. private static synchronized boolean stopRequested() {
  6. return stopRequested;
  7. }
  8. public   static void main(String[] args) throws InterruptedException {
  9. Thread backgroundThread = new Thread(new Runnable() {
  10. @Override
  11. public void run() {
  12. int i = 0;
  13. while (!stopRequested()) {
  14. i++;
  15. }
  16. }
  17. });
  18. backgroundThread.start();
  19. TimeUnit.SECONDS.sleep(1);
  20. requestStop();
  21. }

The mutable data, that is, the state variable stopRequested, is modified by the synchronization method, which ensures that after stopRequested is modified, it can be immediately visible to other threads.

2. Keyword volatile: The most important function of volatile is to ensure data visibility. When one thread modifies variable data, another thread will immediately know the latest data. In the above example, because the read and write of the stopRequested variable are atomic, the use of synchronized only takes advantage of its data visibility. However, since synchronized will lock, if you want better performance, the above example can be modified using volatile:

  1. private static volatile boolean stopRequested;
  2. public   static void main(String[] args) throws InterruptedException {
  3. Thread backgroundThread = new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. int i = 0;
  7. while (!stopRequested) {
  8. i++;
  9. }
  10. }
  11. });
  12. backgroundThread.start();
  13. TimeUnit.SECONDS.sleep(1);
  14. stopRequested = true ;
  15. }

But it should be noted that volatile does not guarantee atomicity, such as the following example:

  1. private static volatile int nextSerialNumber = 0;
  2. public   static   int generateSerialNumber() {
  3. return nextSerialNumber++;
  4. }

Although volatile is used, the ++ operator is not atomic, so it will fail when used with multiple threads. The ++ operator performs two operations: 1. Read the value; 2. Write back the new value (equivalent to the original value + 1). If the second thread reads this field while the first thread is reading the old value and writing the new value, an error will occur and they will get the same SerialNumber. At this time, synchronized is needed to make mutual exclusive access between threads to ensure atomicity.

Summarize

The best way to solve this problem is to avoid sharing mutable data between threads as much as possible and limit mutable data to a single thread. If you want multiple threads to share mutable data, both reading and writing need to be synchronized.

2. Use the method of creating threads with caution

question

Because concurrent programs are prone to thread safety issues and thread management is also very complicated, when creating a thread, do not create it manually through Thread. Instead, use the Executor framework to manage it. What are the advantages of Executor?

Answer

  1. There are various ways to wait for the task to be completed: the current thread can wait for all threads submitted to the executor to complete execution (invokeAll() or invokeAny()), or wait gracefully for the task to end (awaitTermination()), or get the results of these tasks one by one when the tasks are completed (using ExecutorCompletionService), etc.
  2. Create various types of thread pools: You can create a single thread, multiple fixed threads, and a thread pool with a variable number of threads. You can also use the ThreadPoolExecutor class to create a thread pool suitable for the application scenario;
  3. Decoupling between threads and execution: The biggest advantage of using executor is that it decouples the thread execution mechanism from the task. The previous Thread class acts as both a work unit and an execution mechanism, which is better managed and safer and more reliable to use.

in conclusion

When it comes to multi-threaded programs, do not use Thread to create threads. Instead, use executor to manage and create threads. Its biggest advantage is the decoupling between work units (threads) and tasks.

3. Prioritize concurrent tools

question

It is difficult to ensure thread safety in high-concurrency programs, and once a problem occurs, it is also difficult to troubleshoot and analyze the cause. The juc package provides many thread-safe tools. In actual development, we should use more of these tools whose performance has been verified, which makes our development very convenient and ensures the stability of our code. What are the commonly used concurrency tools?

Answer

The concurrent tools under the juc package are divided into three categories: 1. The executor framework responsible for managing threads; 2. Concurrent collections; 3. Synchronizers. Among them, the executor responsible for managing threads has been mentioned in Article 68 and will not be described separately.

  • Concurrent collections: Concurrent collections further process standard collection interfaces (such as List, Queue, and Map) and provide high-performance concurrent implementations. Commonly used ones include CourrentHashMap, which extends the Map interface and ensures thread safety. In addition, BlockingQueue implements blocking operations, that is, when the queue is empty, it will block the "data retrieval" thread until the queue is not empty. When the queue is full, it will block the "data insertion" thread until the queue is not full. BlockingQueue is widely used in "producer-consumer";
  • Synchronizer: Synchronizer can complete the coordination between threads. The most commonly used ones are CountdownLatch and Semaphore, and the less commonly used ones are CyclicBarrier and Exechanger.

in conclusion

The juc package provides us with a variety of thread-safe data structures. In actual development, we should use these tools with guaranteed performance and safety, rather than reinventing the wheel, which is difficult to ensure safety. For example, in the previous code, "producer-consumer" is implemented using wait and notify, which makes the code difficult to maintain. If the BlockingQueue with blocking operations is used, the code will be more concise and the logic will be clearer.

4. Thread safety documentation

question

There are several wrong statements:

These are two common misconceptions. In fact, there are multiple levels of thread safety. So, how should thread safety documentation be established?

  1. Check whether the document contains the synchronized modifier to determine whether the current method is safe. The error in this statement is that synchronized will not be output through javadoc and become part of the API document. This is because synchronized is a specific implementation detail of the method and is not part of the exported API and communication with external modules.
  2. "Any method or code block with the synchronized keyword is thread-safe, and code without this keyword is not thread-safe." This view equates synchronized with thread safety, and believes that there are only two extreme cases of thread safety: either thread-safe or thread-unsafe.

Answer

  • Immutable: Instances of the class are immutable (immutable classes), and must be thread-safe, such as String, Long, BigInteger, etc.
  • Unconditionally ThreadSafe: Instances of this class are mutable, but this class has sufficient internal synchronization. Therefore, its instances can be used concurrently without any external synchronization, such as Random and ConcurrentHashMap.
  • Conditionally ThreadSafe: Some methods need to be synchronized when used externally for thread safety. For example, the collections returned by Collection.synchronized require external synchronization when they are iterated. In the following code, when iterating over the collection returned by synchronizeColletcion, the user must manually synchronize on the returned collection. Failure to follow this advice will result in undefined behavior:
  1. Collection c = Collections.synchronizedCollection(myCollection);
  2. synchronized(c) {
  3. Iterator i = c.iterator(); // Must be in the synchronized block
  4. while (i.hasNext())
  5. foo( i.next ());
  6. }
  • Unthreadsafe: This class is instance-variable. If you want to use it concurrently safely, you must synchronize it manually externally. Such as HashMap and ArrayList;
  • Thread-hostile: Even if all methods are protected by external synchronization, this class cannot be safely used by multiple threads concurrently. There are very few such classes or methods, such as System.runFinalizersOnExit, which is thread-hostile but has been abolished.
  1. Thread safety level:
  2. Be careful when documenting conditionally thread-safe classes, and specify which method calls require external synchronization and which locks need to be acquired;
  3. If the class uses "a publicly accessible lock object", it is very likely that other threads will timeout to keep the publicly accessible lock, causing the current thread to be unable to obtain the lock object. This behavior is called a "denial of service attack". In order to avoid this attack, a private lock object can be used, for example:
  1. private final Object lock = new Object();
  2. public void foo(){
  3. synchronized(lock){
  4. ...
  5. }
  6. }

At this time, the private lock object can only be accessed from within the current class, and cannot be accessed from outside, so it is impossible to interfere with the synchronization of the current class, and "denial of service attacks" can be avoided. However, this method is only suitable for the "unconditional thread safety" level, and cannot be applied to the "conditional thread safety" level. The conditional thread safety level must specify in the document which lock should be obtained when calling a method.

Summarize

Each class should clearly document its thread-safe properties using rigorous instructions or thread-safe annotations. Conditionally thread-safe classes should state which methods require synchronized access and which locks are acquired. Unconditionally thread-safe classes can use private lock objects to prevent "denial of service attacks." When it comes to thread-safe issues, documentation should be written strictly in accordance with the specifications.

5. Use delayed initialization with caution

  • question

Lazy initialization is the act of delaying the initialization of a field until its value is needed. If the value is never needed, the field will never be initialized. This approach applies to both static and instance fields. Like most optimizations, premature optimization is the source of most errors. So what are some reliable ways to achieve thread-safe lazy initialization?

  • Answer

Here is how you would normally initialize instance fields, but note the use of the final modifier:

  1. private final FildType field= computeFieldValue();

Now to lazily initialize this instance field, there are several ways:

1. Synchronization method: When instantiating domain values, you can use synchronization methods to ensure thread safety, such as:

  1. private FieldType field;
  2. synchronized FieldType getField(){
  3. if(field == null ){
  4. field = computeFieldValues();
  5. }
  6. return field;
  7. }

2. Static inner class: In order to reduce the synchronization access cost of the above method, you can use the static inner class method, which is called the lazy initialization holder class mode. Under the optimization of the JVM, this method can not only achieve the effect of lazy initialization, but also ensure thread safety. The sample code is:

  1. private static class FieldHolder{
  2. static final FieldType field = computeFieldValue();
  3. }
  4. static FieldType getField(){
  5. return FieldType.field;
  6. }

3. Double check: This mode avoids the locking overhead when accessing the domain again after initialization (in ordinary methods, synchronized methods are used to synchronize methods, and locks are required every time the method is accessed). The idea of ​​this mode is: check the value of the domain twice, unlock it during the first check to see if it is initialized; lock it during the second check. Only when the second check indicates that it has not been initialized, the computeFieldValue method will be called to initialize it. If it has been initialized, it will not be locked. In addition, it is very important that the domain is declared as volatile. The sample code is:

  1. private volatile FieldType field;
  2. public FieldType getField() {
  3. FieldType result = field;
  4. if (result == null ) {
  5. synchronized (this) {
  6. result = field;
  7. if (result == null ) {
  8. field = result = computeFieldValue();
  9. }
  10. }
  11. }
  12. return result;
  13. }

in conclusion

Most normal initializations are better than lazy initialization. If you must perform lazy initialization, use double detection for instance fields. For static fields, you can use the feature that static inner classes are initialized only when they are first accessed, and use static inner classes to complete lazy initialization.

6. Don’t rely on the thread scheduler

  • question

When there are multiple threads running, the thread scheduler decides which threads will run and allocates CPU time slices. However, the scheduling strategies adopted by most systems are different. Therefore, any concurrent program that relies on the thread scheduler to achieve program performance and correctness is unsafe and non-portable. So, what are some good ways to write portable and robust concurrent programs?

  • Answer
  1. The best way is to ensure that the number of runnable threads is as small as possible, or not significantly higher than the number of processors. If the number of runnable threads is small enough, the thread scheduler does not need to "worry" about which thread to allocate time slices to, and only needs to let the multi-core processor handle these threads. From a side view, it reduces the dependence on the scheduling strategy of the thread scheduler. Then, the only way to ensure the minimum number of threads is to let each thread do meaningful tasks, which will reduce the total number of threads overall;
  2. When the program is incorrect because the thread cannot get enough time slices, do not try to use Thread.yield to let other threads give up time slices to meet your needs. This is because the semantics of Thread.yield are different on different JVMs, which makes it lose portability. In addition, during testing, use Thread.yield to artificially increase thread concurrency. Thread.sleep(1) should be used instead of Thread.yield;
  3. Never attempt to achieve program correctness by adjusting thread priority. Thread priority is the most non-portable feature.
  • in conclusion

Never let your program rely on the thread scheduler, as this will lose robustness and portability. Features such as Thread.yield and thread priority are the least portable and should not be used in your program.

7. Avoid using thread groups

  • question

In addition to threads, locks, and monitors, the thread system also provides another abstract unit: thread groups. The original intention of the thread group design was to isolate applets and achieve security. However, in reality, the expected security is not achieved, and it is so poor that it is not even mentioned in the JAVA security model. In addition to the poor security, what other defects are there?

  • Answer

Besides the security not being up to expectations, there are few basic features available;

The ThreadGroup API is very fragile;

  • in conclusion

Thread groups don't provide a lot of useful functionality, and many of the features they do provide are flawed. When managing threads or dealing with thread group logic, you should consider using executors.

<<:  The US court's suspension of the TikTok ban will take effect this Sunday

>>:  Three-minute review! A quick overview of 5G industry development trends in September

Recommend

Are enterprises ready for open RAN?

The increasing deployment of 5G has brought about...

Development Trend of International 5G Private Network Applications

As an important driving force for the digital tra...

With the arrival of 5G, will enterprise-level networks disappear?

Reader Question: Although I am also in the IT ind...

Use data to tell you the current status of IPv6 development in China in 2021

A few days ago, Xiao Wei shared with everyone the...

Analysis: Advantages and limitations of wireless data centers

For data center operators, the idea of ​​a wirele...

Learn about FTP/FTPS/SFTP file transfer protocols in one article

Introduction to FTP FTP (File Transfer Protocol) ...

Next-generation data center connectivity for 400G and beyond

[[393969]] The data center industry is experienci...