Reading notes-java concurrent programming combat-Chapter 2 thread safety

The premise for thread safety issues:

This variable can be modified

The variable is accessed by multiple threads

When multiple threads access the same variable state variable without proper synchronization, an error will occur in the program. There are three ways to solve this problem:

  • Do not share the state variable between threads
  • Modify state variables to immutable variables
  • When visiting, use sync

1. What is thread safety?

Thread safety: When multiple threads access a certain class, the class can behave correctly, then we call this class thread-safe.

Stateless objects must be thread-safe

(Stateful and stateless: Stateful means a class with the function of storing data. Stateless means that the attributes in the class do not store data)

2. Atomicity

1. Race condition: When multiple threads access the same resource, the different order of execution of the threads will result in inconsistent results. At this time, a race condition occurs. Code resources accessed by multiple threads are called critical sections.

  • Check first and then execute: make a judgment based on an observation that may fail, or perform a certain operation. This kind of race condition is called check before execution.

2. Race conditions in lazy initialization : lazy initialization will initialize the object when it is needed to save memory. At the same time, we must ensure that the object is initialized only once. However, in the case of multithreading, two threads execute the initialization condition at the same time. When the initialization of the first thread is not completed, the second thread will perform the condition judgment at this time. The result is that the object has not been initialized, then the second thread will perform initialization again. Thread safety issues have occurred at this time. The code that should have been executed once was executed once by two threads. If there is another thread to judge the initialization conditions before the initialization of the 1st and 2nd threads is completed. Then one more initialization will be performed. This is a situation we don't want to see.

3. Compound operation: The combination of several actions that must be performed atomically is called compound operation. For example, check first and then execute, there is a set of actions to be executed atomically. So check first and then execute is a compound operation. If we want to ensure thread safety, we must ensure that each group of compound operations are executed atomically. Generally speaking, we will achieve this goal through locking.

Three, locking mechanism

1. Built-in lock: Java provides a built-in lock mechanism to support atomicity: synchronized code blocks. The synchronization code block consists of two parts: one is the object reference of the lock; the other is the protection code block of the lock. The method modified with the keyword Synchronized is a synchronization method that includes the entire method body. The lock of this code block is an instance of calling this method. That is this. When Synchronized modification is a static method, the lock of the code block is the .CLASS object of the current class.

2. Reentrant: When a thread requests a lock held by another thread, the requesting thread will be blocked. However, because the internal lock is reentrant, if a thread tries to acquire a lock it already holds, the request will succeed. One way to achieve reentrancy is to associate the lock with a counter and an owner thread. When the counter is 0, the lock is not held by any thread. At this time, any thread that applies for the lock can succeed. At the same time, the JVM will record the holder of the lock and set the counter to 1. When the same thread again Acquiring this lock, the counter will increment, and when the thread exits the synchronization code block, the counter will decrement until the counter reaches 0 and the lock is released.

3. The benefits of reentry : When the subclass overrides the synchronization method of the parent class, as shown in the following code. When executing doSomething of the subclass LoggingWidget, the thread will get the lock. When the doSomething of the subclass is executed, the doSomething of the parent is called again. If the built-in lock is not reentrant, the doSomething of the parent class will wait forever. Reentry avoids this kind of deadlock.

public class Widget{
    
    
    public synchronized void doSomething(){
    
    
        ...
    }
}

public class LoggingWidget extends Widget{
    
    
    public synchronized void doSomething(){
    
    
        system.out.println("=================");
        super.doSomething();
    }
}

Fourth, use a lock to protect the state

Variables can be protected by locks, which ensures that only one thread is operating this variable at a time. Only when the thread completes the operation on the variable, other threads can operate on the variable.

5. Activity and performance

  • Locking can indeed ensure the security of our program. We need to consider security because: multi-threading is used in order to improve performance. In the case of multi-threading, thread safety issues will occur, so we need to lock to ensure synchronization. But this does not mean that we have to sacrifice performance for safety. Therefore, we should ensure the security of the program while ensuring performance. Don't sacrifice performance for blind security. Then we introduce multithreading and it doesn't make any sense.
  • Therefore, when we lock, we must make a reasonable judgment on the size of the synchronization code block. At this point, we need to find a trade-off between security, simplicity, and performance. There are often conflicts between simplicity and performance.

Note: When performing long-term calculations or operations that may not be completed quickly (network I/O or console I/O), do not hold the lock

Guess you like

Origin blog.csdn.net/weixin_45373852/article/details/108726264