Double check locking is often found in framework source code to delay initialization of variables. This pattern can also be used to create singletons. Let’s look at an example of double-checked locking in Spring.
In this example, you will need to load the configuration file to handlerMappings. Since reading the resource takes time, save the action for when handlerMappings is really needed. We can see that volatile is used in front of handlerMappings. Ever wonder why volatile is necessary? Although we have seen how the double-checked locking pattern works, the use of volatile was ignored.
Here’s why.
Bad example of delayed initialization
The simplest example of thinking about lazy initialization of a variable is fetching the variable to make a judgment.
This example works fine in a single-threaded environment, but a null-pointer exception might be thrown in a multi-threaded environment. To prevent this, use synchronized. This method is safe in multithreaded environments, but doing so results in expensive locks being acquired and released each time the method is called.
Further analysis shows that only the initialized variables need to be locked, and once initialized, the object is returned directly.
So we can adapt the method to look like this.
This method first determines whether the variable is initialized, not initialized, and then acquires the lock. Once the lock is acquired, determine again whether the variable is initialized. The second determination is that it is possible that another thread has acquired the lock and initialized the change. The variable is not actually initialized until the second check has passed.
This method checks to decide twice and uses the lock, so the image is called double-checked locking mode.
This solution reduces the scope of locks and the overhead of locks, which seems perfect. Yet there are some problems with the scheme that are easy to overlook.
The instruction behind the new instance
The overlooked problem is that Cache Cache =new Cache() is not an atomic instruction. Using the Javap -C directive, you can quickly view the bytecode.
// create a Cache instance, allocate memory 0: new #5 // class com/query/Cache // copy the top address of the stack and push it to the top 3: dup // call the constructor method, initialize the Cache object 4: Invokespecial #6 // Method "<init>":()V // store local Method variable table 7: ASTORE_1Copy the code
From the bytecode, you can see that creating an object instance can be divided into three steps:
- Allocating object memory
- Call the constructor method to perform the initialization
- Assigns an object reference to a variable.
The above instructions may be reordered when the virtual machine is actually running. Code 2,3 above may be reordered, but not the order of 1. That is to say, all instructions 1 need to be executed first, because instructions 2,3 need to rely on the execution result of instruction 1.
The Java language specifies that threads need to comply with intra-thread Semantics when executing programs. ** Intra-thread Semantics ** Guarantees that reordering does not alter the execution of a program in a single thread. This reordering can improve the performance of a single-threaded program without changing its execution results.
Although reordering does not affect the results of execution in a single thread, it presents some problems in a multi-threaded environment.
In the example code above for incorrectly double-checked locking, instruction reordering occurs when thread 1 acquires the lock and enters the creation of the object instance. When thread 1 executes to t3, thread 2 just enters. Since the object is no longer Null, thread 2 can access the object freely. Then the object is not initialized, so an exception will occur when thread 2 accesses it.
Volatile role
The correct double-checked locking pattern requires the use of volatile. Volatile has two main functions.
- Ensure visibility. use
volatile
Defines variables that will ensure visibility to all threads. - Disallow instruction reordering optimization.
Because volatile forbids reordering between instructions at object creation, an uninitialized object cannot be accessed by other threads, ensuring security.
Note that volatile prohibited instruction reordering until JDK 5
Optimize performance with local variables
Revisit the double-checked locking code in Spring.
You can see that the method uses local variables internally, first assigning the instance variable value to the local variable, and then making a judgment. Finally, the content is written to the local variable and then assigned to the instance variable.
Using local variables improves performance compared to not using local variables. The main reason for this is that the instruction reorder is prohibited when volatile variables create objects, which requires some additional operations.
conclusion
Object creation can result in instruction reordering. Using volatile can prevent instruction reordering and ensure system security in multithreaded environments.
Help document
Double-checked locking and delayed initialization explains “double-checked locking failure”