Double check locking and deferred initialization

Source: Internet
Author: User

In Java programs, there may be times when you need to defer some high-overhead object initialization operations and initialize them only when you are working with those objects. At this point the programmer may be using deferred initialization. However, to properly implement thread-safe lazy initialization requires some skill, otherwise it is prone to problems. For example, here is a sample code for a non-thread-safe Deferred initialization object :

 public  class   unsafelazyinitialization { private  static   Instance Instance;  public  static   Instance getinstance () { if  (Instance = = Span style= "COLOR: #0000ff" >null ) //  1:    A thread executes  instance = new  instance (); // 2:b thread execution  return   instance; }}

In Unsafelazyinitialization, assume that the a thread executes code 1 while the B thread executes code 2. At this point, thread A may see that the object referenced by instance has not yet been initialized (the cause of this scenario is "the source of the problem" later in this article).

For unsafelazyinitialization, we can do synchronous processing of getinstance () to implement thread-safe deferred initialization. The sample code is as follows:

 public  class   safelazyinitialization { private  static   Instance Instance;  public  synchronized  static   Instance getinstance () { if  (Instance = = null  ) instance = new   Instance ();     return   instance; }}

Due to the synchronous processing of getinstance (), synchronized will result in a performance overhead. If getinstance () is called frequently by multiple threads, it will degrade the performance of the program execution, whereas if getinstance () is not frequently called by multiple threads, this deferred initialization scheme will provide satisfactory performance.

In earlier JVMs, synchronized (even non-competitive synchronized) had this huge performance overhead. As a result, people came up with a "smart" technique: double-check locking (double-checked locking). People want to reduce the overhead of synchronization by double-checking locks. The following is a sample code that uses a double-check lock to implement lazy initialization:

 Public classdoublecheckedlocking {//1    Private StaticInstance Instance;//2     Public StaticInstance getinstance () {//3        if(Instance = =NULL) {//4: First check            synchronized(doublecheckedlocking.class) {//5: Locking                if(Instance = =NULL)//6: Second checkInstance =NewInstance ();//7: The root of the problem is here}//8}//9        returnInstance//Ten}// One}// A

As shown in the preceding code, if the first check for instance is not NULL, then the following locking and initialization operations are not required. As a result, the performance overhead of synchronized can be greatly reduced. The above-mentioned code looks as if it's both worlds:

    • When multiple threads attempt to create an object at the same time, a lock is made to guarantee that only one line Cheng Nen create the object.
    • After the object is created, execution getinstance () will not need to acquire the lock and return the created object directly.

Double check lock looks perfect, but it's a mistake to optimize! When a thread executes to the 4th line of code read to instance is not NULL, the object referenced by instance may not have completed initialization.

The root of the problem

The previous double check locks the 7th line of the sample code (instance = new Singleton ();) creates an object. This line of code can be decomposed into the following three lines of pseudo-code:

Memory = allocate ();   1: Allocating the object's memory space ctorinstance;  2: Initialize object instance = memory;     3: Set instance to point to the memory address just allocated

The above three lines of pseudo code between 2 and 3, may be reordered (on some JIT compilers, this reordering is true, the details are described in reference 1, "Out-of-order writes" section). The execution timing after reordering between 2 and 3 is as follows:

Memory = allocate ();   1: Allocating the object's memory space instance = Memories;     3: Set instance point to the memory address you just allocated  note that the object has not been initialized at this time! Ctorinstance (memory);  2: Initializing objects

According to the Java Language specification, Java SE 7 Edition (hereinafter referred to as the Java Language Specification), all threads must adhere to intra-thread semantics when executing Java programs. Intra-thread semantics ensures that reordering does not change the results of program execution within a single thread. In other words,intra-thread semantics allows those within a single thread to not change the reordering of the results of a single-threaded session. Although the above three lines of pseudo-code between 2 and 3 are reordered, this reordering does not violate intra-thread semantics. This reordering can improve the execution performance of the program without changing the execution results of the single-threaded programs.

To better understand intra-thread semantics, look at the following (assuming that a thread A is accessing the object immediately after the object is constructed):

As shown, as long as the 2 row in front of 4, even if the reordering between 2 and 3, will not violate intra-thread semantics.

Next, let's look at what happens when multithreading is executed concurrently. Take a look at the following:

Because the intra-thread semantics must be adhered to in a single thread, the program execution results of the a threads are guaranteed to be unchanged. However, when threads A and b press the timing of execution, theb thread will see an object that has not yet been initialized .

※ Note: This article unifies the red imaginary arrow line to identify the wrong reading operation, and uses the green imaginary arrow line to identify the correct reading operation.

Returning to the topic of this article, doublecheckedlocking the 7th line of the sample code (instance = new Singleton ();) If a reorder occurs, another thread B that executes concurrently may be able to determine instance not null on line 4th. Thread B Next accesses the object referenced by instance, but at this point the object may not have been initialized by a thread! Here is the specific execution sequence for this scenario:

Time

Thread A

Thread B

T1

A1: Allocating memory space for an object

T2

A3: Set instance point to memory space

T3

B1: Determine if the instance is empty

T4

B2: Because instance is not NULL, thread B accesses the object referenced by instance

T5

A2: Initializing objects

T6

A4: Accessing objects referenced by instance

Here A2 and A3 are reordered, but the intra-thread semantics of the Java memory model will ensure that A2 is bound to be executed in front of A4. So the intra-thread semantics of thread A does not change. However, the reordering of A2 and A3 will cause thread B to determine that instance is not empty at B1, and thread B will then access instance referenced objects. At this point, thread B will have access to an uninitialized object.

After knowing the root cause of the problem, we can think of two ways to implement thread-safe deferred initialization:

    1. 2 and 3 reordering are not allowed;
    2. Allow 2 and 3 to reorder, but not allow other threads to "see" this reordering.

The following article describes the two solutions, respectively, corresponding to the above two points.

Volatile-based double-check locking solution

For the previous scenario based on double-check locking to implement deferred initialization (referred to as the Doublecheckedlocking sample code), we only need to make a small modification (declaring the instance as volatile) to allow for thread-safe deferred initialization. Take a look at the following sample code:

 Public classsafedoublecheckedlocking {Private volatile StaticInstance Instance;  Public StaticInstance getinstance () {if(Instance = =NULL) {            synchronized(safedoublecheckedlocking.class) {                if(Instance = =NULL) Instance=NewInstance ();//instance is volatile, it's no problem now.            }        }        returninstance; }}

Note that this solution requires JDK5 or higher (since the new JSR-133 memory model specification is used from JDK5, this specification enhances the semantics of volatile).

When declaring an object's reference as volatile, the "root cause of the problem" in three lines of pseudo-code between 2 and 3 is reordered in a multithreaded environment that will be banned . The example code above will be executed as follows:

This scheme is essentially to ensure thread-safe lazy initialization by banning the reordering between 2 and 3.

Class-Based initialization solutions

The

JVM executes the initialization of the class at the initialization stage of the class (that is, after class is loaded and before it is used by the thread). During initialization of the class, the JVM goes to fetch a lock. This lock synchronizes the initialization of multiple threads to the same class. Based on this feature, another thread-safe deferred initialization scheme can be implemented (this scheme is called initialization on Demand Holder idiom ):

 Public class instancefactory {    privatestaticclass  instanceholder {          publicstaticnew  Instance ();    }      Public Static Instance getinstance () {        return instanceholder.instance;  // this will cause the Instanceholder class to be initialized     }}

Assuming that two threads execute getinstance () concurrently, the following is performed:

The essence of this scenario is to allow the "root cause of the problem" in three lines of pseudo-code to reorder 2 and 3, but not to allow non-constructed threads (here, thread B) to "see" this reordering.

Initializes a class that includes static initialization of the class and initializes static fields declared in this class. According to the Java language Specification, a class or interface type T is immediately initialized when either of the following conditions is first encountered:

    • T is a class, and an instance of type T is created;
    • T is a class, and a static method declared in T is called;
    • A static field declared in T is assigned value;
    • A static field declared in T is used, and this field is not a constant field;
    • T is a top level class, see the Java language Specification for §7.6, and an assertion statement nested inside T is executed.

In the Instancefactory sample code, the thread that first executes getinstance () causes the Instanceholder class to be initialized (in accordance with case 4).

Because the Java language is multithreaded, multiple threads may attempt to initialize the same class or interface at the same time (for example, where multiple threads may call getinstance () at the same moment to initialize the Instanceholder class). Therefore, when initializing a class or interface in Java, careful synchronization is required.

The Java language Specification specifies that for each class or interface C, there is a unique initialization lock LC corresponding to it. The mapping from C to LC is implemented freely by the specific implementation of the JVM. The JVM acquires this initialization lock during class initialization, and each thread acquires at least one lock to ensure that the class has been initialized (in fact, the Java language Specification allows a specific implementation of the JVM to do some optimizations here, as described later in this article).

For initialization of classes or interfaces, the Java language specification provides a sophisticated and complex class initialization process. Java initializes the processing of a class or interface as follows (here is a description of the class initialization process, omitting the parts unrelated to this article, and in order to better illustrate the synchronous processing mechanism during class initialization, the author has divided the process of class initialization into five stages):

First stage: Controls the initialization of a class or interface by synchronizing on a class object (that is, getting the initialization lock of a class object). The thread that acquires the lock waits until the current thread is able to acquire the initialization lock.

Suppose that the class object is not currently initialized (the state of initialization is now marked as noinitialization), and two threads A and B attempt to initialize the class object at the same time. The following are the corresponding:

Here is a description of this:

time

Thread A

thread B

T1

A1: Attempt to get initialization lock for class object. This assumes that thread a gets the initialization lock

B1: Try to get the class object's initialization lock, because thread a acquires the lock, thread B waits for the initialization lock

t2

A2: Thread A sees that the thread has not been initialized (because it reads to state = = noinitialization), the thread is set to state = initializing

 

T3

A3: thread A releases the initialization lock

 

The second stage: thread A executes the initialization of the class, while thread B waits on the condition corresponding to the initialization lock:

Here is a description of this:

in the condition of the initialization lock

time

Thread A

thread B

T1

A1: Static initialization of the execution class and initialization of statically declared fields in class

B1: get to initialize lock

T2

 

B2: Read to state = = initializing

T3

 

B3: release initialization lock

T4

 

B4: Wait for

Phase three: Thread A sets state = initialized, and then wakes up all the threads waiting in condition:

Here is a description of this:

Time

Thread A

T1

A1: Get initialization lock

T2

A2: Setting state = initialized

T3

A3: Wake up all threads waiting in condition

T4

A4: Release initialization lock

T5

A5: Initialization process for thread A is complete

Phase four: Thread B ends initialization of the class:

Here is a description of this:

Time

Thread B

T1

B1: Get initialization lock

T2

B2: Read to state = = Initialized

T3

B3: Release initialization lock

T4

B4: Class initialization process for thread B is complete

Thread A initializes the class in the second stage of the A1 and releases the initialization lock in the third stage of the A4, and thread B acquires the same initialization lock on the B1 of phase fourth, and then accesses the class after the fourth stage of B4.

Depending on the lock rules of the Java memory Model specification, the following happens-before relationships are present:

This happens-before relationship will guarantee that thread a writes when the class is initialized (performing static initialization of the class and initializing the static field declared in the Class), and thread B must be able to see it.

Phase Fifth: Thread C performs initialization processing of classes:

Here is a description of this:

Time

Thread B

T1

C1: Get initialization lock

T2

C2: Read to state = = Initialized

T3

C3: Release initialization lock

T4

C4: Class initialization process for thread C is complete

After the third phase, the class has completed initialization. Therefore, the class initialization process for thread C in phase fifth is relatively straightforward (the class initialization process for the preceding threads A and B has undergone two lock acquisition-lock releases, while class initialization for thread C only needs to undergo a lock acquisition-lock release).

Thread A initializes the class in the second stage of the A1 and releases the lock in the third phase of the A4, and thread C acquires the same lock in the fifth phase of C1, and then accesses the class after the fifth stage of C4.

Depending on the lock rules of the Java memory Model specification, the following happens-before relationships are present:

This happens-before relationship will guarantee that thread a writes the initialization of the class, and thread C must be able to see it.

※ Note 1: The condition and state marks here are fictitious in this article. The Java language specification does not require the use of condition and state tags. A specific implementation of the JVM can only be implemented with similar functionality.

※ Note The 2:java language specification allows Java to be implemented in a specific way, optimizing the initialization process of the class (for the fifth phase of the optimization), as detailed in the Java language Specification 12.4.2.

By comparing the scenarios of volatile-based double-check locking and class-based initialization, we find that the implementation code for a scenario based on class initialization is more concise . However, a volatile-based double-check locking scheme has an additional advantage: You can defer initialization of instance fields, in addition to delaying the initialization of static field implementations.

Summarize

Lazy initialization reduces the overhead of initializing classes or creating instances, but increases the cost of accessing fields that are lazily initialized. Most of the time, normal initialization is better than lazy initialization. If you do need to use thread-safe deferred Initialization for instance fields, use the volatile-based deferred initialization scenario described above, and if you do need to use thread-safe deferred initialization for static fields, use the class-based initialization scenario described above.

Reference documents
    1. double-checked locking and the Singleton pattern
    2. The Java Language specification, Java SE 7 Edition
    3. Jsr-133:java Memory Model and Thread specification
    4. Java Concurrency in practice
    5. Effective Java (2nd Edition)
    6. JSR 133 (Java Memory Model) FAQ
    7. The JSR-133 Cookbook for Compiler writers
    8. Java theory and practice:fixing the Java Memory Model, part 2

Double check locking and deferred initialization

Double check locking and deferred initialization

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.