📜 ⬆️ ⬇️

JSR 133 (Java Memory Model) FAQ (translation)

Good day.
As part of the recruitment course for “Multicore programming in Java”, I am doing a series of translations of classic articles on multithreading in Java. Every study of multithreading should begin with an introduction to the Java memory model (New JMM), the main source from the authors of the model is “The Java Memory Model” home page , where it is proposed to get acquainted with the JSR 133 (Java Memory Model) FAQ . That's with the translation of this article, I decided to start the series.
I allowed myself a few inserts "from myself", which, in my opinion, clarify the situation.
I am an expert in Java and multithreading, not a philologist or translator, therefore I allow certain liberties or reformulations during translation. In case you offer the best option, I’m happy to edit it.
This article is also suitable as an educational material for the lecture “Lecture # 5.2: JMM (volatile, final, synchronized)” .

I also teach Scala for Java Developers on the udemy.com online education platform (equivalent to Coursera / EdX).

Well, yes, come to study with me!


JSR 133 (Java Memory Model) FAQ


Jeremy Manson and Brian Goetz, February 2004
')
Content:
What is a memory model, after all?
Do other languages ​​like C ++ have a memory model?
What is JSR 133?
What is meant by “reordering”?
What was wrong with the old memory model?
What do you mean by "incorrectly synchronized"?
What does synchronization do?
How can it happen that the final fields change values?
How do the final fields work under the new JMM?
What does volatile do?
Has the new memory model "double-checked locking" solved the problem?
What if I write a virtual machine?
Why should I worry?


What is a memory model, after all?


In multiprocessor systems, processors usually have one or more cache layers, which improves performance by either speeding up data access (because the data is closer to the processor) or by reducing traffic on the shared memory bus (since many memory operations can be satisfied with local caches.) Caches can dramatically improve performance, but their use throws many new challenges. What, for example, happens when two processors view the same memory cell at the same time? Under what conditions will they see the same values?

At the processor level, the memory model defines the necessary and sufficient conditions to ensure that the other processors' memory records are visible to the current processor, and the current processor's records are visible to other processors. Some processors demonstrate a strong memory model, where all processors always see exactly the same values ​​for any given memory location. Other processors demonstrate a weaker memory model, where special instructions, called memory barriers, are required to “flush” or invalidate data in the processor’s local cache in order to make the processor’s records visible to others or to see the records made other processors. These memory barriers are usually executed when locking (lock) and releasing (unlock) the lock; they are invisible to programmers in high-level languages.

Sometimes it is easier to write programs for strong memory models, because of the reduced need for barriers. However, even on the strongest memory models, barriers are often necessary; quite often their placement is contrary to intuition. Recent processor design trends encourage weaker memory models, because the cuts that they make to cache consistency provide increased scalability across multiple processors and large amounts of memory.

The question of when a record becomes visible to another thread is compounded by the reordering of instructions by the compiler. For example, the compiler may decide that it is more efficient to move the write operation further in the program; as long as moving the code does not change the semantics of the program, he can do it freely. If the compiler delays the operation, the other thread will not see until it is implemented; This demonstrates the effect of caching.

In addition, an entry in memory can be moved earlier in the program; in this case, other threads may see the recording before it actually "happens." All this flexibility is a design feature — by giving the compiler, runtime, and hardware the flexibility to perform operations in an optimal order within the memory model, we can achieve higher performance.

A simple example of this can be seen in the following code snippet:
class Reordering { int x = 0, y = 0; public void writer() { x = 1; y = 2; } public void reader() { int r1 = y; int r2 = x; } } 


Let's assume that this code is executed in two threads at the same time and reading 'y' returns a value of 2. Because this record is located after writing in 'x', the programmer may assume that reading 'x' should return value 1. However, writing The 'x' and 'y' may have been reordered. If this was the case, then writing to 'y' could occur, then reading both variables, and only then writing to 'x'. The result is that r1 is 2, and r2 is 0.
Translator comment
It is assumed that in the same object the reader () method and the writer () method “almost simultaneously” are called from different streams.

The Java memory model describes which behavior is legal in multi-threaded code and how threads can interact through memory. It describes the relationships between variables in the program and the low-level details of storing and retrieving them to and from memory or registers in a real computer system. The model defines it in such a way that it can be implemented correctly using a wide range of hardware and a wide variety of compiler optimizations.

Java includes several language constructs, including volatile, final, and synchronized, which are intended to help the programmer to describe to the compiler the requirements for concurrency in the program. The Java memory model defines volatile and synchronized behavior, and, more importantly, ensures that a correctly synchronized Java program works correctly on all processor architectures.

Do other languages ​​like C ++ have a memory model?


Most other programming languages, such as C and C ++, were not designed with direct support for multithreading. The protective measures that these languages ​​offer against various types of reordering occurring in compilers and processors largely depend on the guarantees offered by the parallelization libraries used (for example, pthreads) used by the compiler and the platform on which the code is run.
Translator comment
Java has set the trend for introducing a memory model into a language specification and the latest C ++ standard already has a memory model (Chapter 1.7, “The C ++ Memory Model”) . It seems it is already in C11.


What is JSR 133?


Since 1997, several serious flaws have been discovered in the Java memory model, which is defined in chapter 17 of the language specification. These flaws allowed shocking behavior (for example, changing the value of a final-field was allowed) and prevented the compiler from using typical optimizations.
Translator comment
The old memory model (Old JMM, before Java 5) was described exclusively in Chapter 17 of the “Chapter 17. Threads and Locks” language specification.
The new memory model (New JMM, starting with Java 5) is described both in Chapter 17 “Chapter 17. Threads and Locks” in the language specification and is expanded in a separate document (JSR-133).
New jmm
Old JMM:


The Java memory model was an ambitious project; for the first time, a programming language specification attempted to include a memory model that can provide consistent semantics for parallelism among various processor architectures. Unfortunately, defining a memory model that is both consistent and intuitive has proven to be much more difficult than expected. JSR 133 defines a new memory model for Java that corrects the flaws of the previous model. In order to do this, the semantics of final and volatile has been changed.

A full description of the semantics is available at http://www.cs.umd.edu/users/pugh/java/memoryModel , but the formal description is not for the timid. It is surprising and sobering when you find out how difficult such a simple concept as synchronization really is. Fortunately, you do not need to understand all the details of formal semantics — the goal of JSR 133 was to create a set of rules that provides an intuitive understanding of how volatile, synchronized, and final work.
Translator comment
The author gives a link to the “home page” of the Java Memory Model - the main source of information on the Internet.

Translator comment
Formal semantics makes it possible to calculate all possible scenarios for the behavior of an arbitrary, both correctly synchronized and incorrectly synchronized program.
In this article, the details of the formal semantics will not be considered, only some intuitive description will be given.

The objectives of JSR 133 include:


What is meant by “reordering”?


There are a number of cases in which access to variables (fields of objects, static fields, and array elements) can be performed in a different order than specified in the program. The compiler is free to arrange instructions for optimization. Processors may execute instructions in a different order in some cases. Data can be moved between registers, processor caches and RAM in a different order than specified in the program.
Translator comment
Further, the term variables will refer to the fields of objects, static fields and array elements.

For example, if a thread writes in the 'a' field and then in the 'b' field and the value of 'b' does not depend on the value of 'a', then the compiler is free to change the order of these operations, and the cache has the right to flush the 'b' into RAM earlier than 'a'. There are several potential sources of reordering, such as the compiler, JIT and cache memory.

The compiler, runtime, and hardware allow for reordering of instructions while maintaining the illusion of how-if-serial (as-if-serial) semantics, which means that single-threaded programs should not observe the effects of reordering. However, reordering may come into play in incorrectly synchronized multi-threaded programs, where one thread can observe the effects produced by other threads, and such programs may be able to detect that variables become visible to other threads in a different order than specified in source code.

Most of the time, threads do not take into account what other threads are doing. But when they need it, synchronization comes into play.

What was wrong with the old memory model?


There were several serious problems with the old memory model. She was difficult to understand and therefore often violated. For example, the old model in many cases did not allow many types of reordering, which were implemented in each JVM. This confusion with the meaning of the old model led to the fact that they were forced to create a JSR-133.

It was widely believed that when using the final-field there was no need for synchronization between the threads to ensure that the other stream would see the value of the field. Although this is a reasonable assumption about rational behavior, and indeed we would like everything to work that way, but under the old memory model, this was simply not true. Nothing in the old memory model distinguishes the final-field from any other data in memory — this means that synchronization was the only way to ensure that all threads see the value of the final-field written in the constructor. As a result, it was possible that you would see the default value of the field, and then after a while you would see the assigned value. This means, for example, that immutable objects, such as strings, can change their value — a disturbing perspective.
Translator comment
An example of strings will be explained in detail below.

The old memory model made it possible to change the order between writing to volatile and reading / writing normal variables, which is inconsistent with most developers' understanding of volatile and therefore confusing.
Translator comment
Below are examples with volatile

Finally, the assumptions of programmers about what can happen if their programs are incorrectly synchronized are often erroneous. One of the goals of JSR-133 is to pay attention to this fact.

What do you mean by "incorrectly synchronized"?


By incorrectly synchronized code, different people mean different things. When we talk about incorrectly synchronized code in the context of the Java memory model, we mean any code in which:
  1. there is a variable record in one stream,
  2. there is a reading of the same variable by another thread and
  3. read and write are not ordered by synchronization (are not ordered by synchronization)

When this happens, we say that there is a data race on this variable. Programs with race streams are incorrectly synchronized programs.
Translator comment
We must understand that incorrectly synchronized programs are not Absolute Evil. Although their behavior is non-deterministic, all possible scenarios are fully described in JSR-133. These behaviors are often non-intuitive. The search for all possible results is algorithmically very complicated and relies, among other things, on such new concepts as the commitment protocol and causality loops.
But in some cases, the use of incorrectly synchronized programs seems to be justified. As an example, it suffices to give an implementation of java.lang.String.hashCode ()
 public final class String implements Serializable, Comparable<String>, CharSequence { /** Cache the hash code for the string */ private int hash; // Default to 0 ... public int hashCode() { int h = hash; if (h == 0 && count > 0) { ... hash = h; } return h; } ... } 

When the hashCode () method is called, one instance of java.lang.String from different streams will have a data race across the hash field.

Interesting article Hans-J. Boehm, "Nondeterminism is unavoidable, but data races are pure evil"
And its discussion in Russian
Ruslan Cheremin, "Data races is pure evil"


What does synchronization do?


Synchronization has several aspects. The most well understood is mutual exclusion - only one thread can own the monitor, so synchronization on the monitor means that as soon as one stream enters the synchronized block protected by the monitor, no other stream can enter the block protected by this monitor until the first thread exits the synchronized block.

But synchronization is more than just mutual exclusion. Synchronization ensures that data stored in memory before or in a synchronized block becomes predictably visible to other threads that are synchronized on the same monitor. After we exit the synchronized block, we release the monitor, which has the effect of flushing the cache into RAM, so the recordings made by our stream may be visible to other threads. Before we can enter the synchronized block, we capture (asquire) the monitor, which has the effect of invalidating the local processor cache data, so the variables will be loaded from the main memory. Then we will be able to see all the records made visible by the previous release of the monitor.

Discussing the situation in terms of caches, it may seem that these issues affect only multiprocessor machines. However, the effects of reordering can easily be seen on a single processor. The compiler cannot move your code until the monitor is captured or after it is released. When we say that capturing and releasing monitors affects caches, we use an abbreviation for a number of possible effects.

The semantics of the new model of memory imposes a partial order on memory operations (reading a field, writing a field, locking a lock (lock), releasing a lock (unlock)) and other operations with threads (start (), join ()). Some actions, as they say, “occur before” (happens before) others. When one action “happens before” (happens before) another, the first will be guaranteed to be located before and visible to the second. The rules for this ordering are:
Translator comment
Partial order is not a turn of speech, but a mathematical concept.

  1. Each action in the stream “happens before” (happens before) any other action in this stream that goes “below” in the code of this stream.
  2. The release of the monitor “happens before” (happens before) each subsequent capture of the same monitor .
  3. Writing to the volatile-field occurs “happens before” (happens before) each subsequent reading of the same volatile-field .
  4. Calling the start () method of the thread “happens before” (happens before) any actions in the launched thread.
  5. All actions in the stream “occur before” (happens before) any actions of any other stream that successfully completed the wait on join () on the first thread.

This means that any memory operations that were visible in the stream before exiting the synchronized block are visible to any other stream after it enters the synchronized block protected by the same monitor, since all memory operations "will occur before" the monitor is released , and the release of the monitor "occurs before" capture.
Translator comment
This program (data - volatile, run - volatile) is guaranteed to stop and print 1 And in the old And in the new memory models
 public class App { static volatile int data = 0; static volatile boolean run = true; public static void main(String[] args) { new Thread(new Runnable() { public void run() { data = 1; run = false; } }).start(); while (run) {/*NOP*/}; System.out.println(data); } } 


This program (data - NOT volatile, run - volatile) is guaranteed to stop And in the old And in the new memory model, but in the old it can print both 0 and 1, and in the new one it will guarantee 1. This is due to the fact that in the new memory model It is possible to “raise” an entry in non-volatile, “higher” entries in volatile, but not “lower down”. And in the old one it was possible to both “raise” and “lower down”.
 public class App { static int data = 0; static volatile boolean run = true; public static void main(String[] args) { new Thread(new Runnable() { public void run() { data = 1; run = false; } }).start(); while (run) {/*NOP*/}; System.out.println(data); } } 


This program (data - volatile, run - NOT volatile) can either stop or not in both models. In the case of a stop, it can print both 0 and 1 in both the old and new memory models. This is due to the fact that in both models it is possible to “raise” an entry in the non-volatile above the entry in the volatile.
 public class App { static volatile int data = 0; static boolean run = true; public static void main(String[] args) { new Thread(new Runnable() { public void run() { data = 1; run = false; } }).start(); while (run) {/*NOP*/}; System.out.println(data); } } 


This program (data - NOT volatile, run - NOT volatile) may or may not stop in both models. In the case of a stop, it can print both 0 and 1 in both the old and new memory models.
 public class App { static int data = 0; static boolean run = true; public static void main(String[] args) { new Thread(new Runnable() { public void run() { data = 1; run = false; } }).start(); while (run) {/*NOP*/}; System.out.println(data); } } 


Another consequence is that the following pattern, which some people use to install a memory barrier, does not work:
 synchronized (new Object()) {} 

This construction is actually a “dummy” (no-op), and your compiler can delete it completely, because the compiler knows that no other stream will be synchronized on the same monitor. You must establish a “going before” relationship for one thread to see the results of another.

Important note : Please note that it is important for both streams to synchronize on the same monitor in order to establish the relationship “happens before” properly. This is not the case when everything visible to thread A, when it is synchronized on object X, becomes visible to stream B after it synchronizes on object Y. Release and capture must “match” (that is, be performed with the same monitor ) to ensure proper semantics. Otherwise, the code contains a data race.
Translator comment
The next program can either stop or not stop within the framework of both memory models (since monitors of various objects are captured and released in different threads - lockA / lockB)
 public class App { static Object lockA = new Object(); static Object lockB = new Object(); static boolean run = true; public static void main(String[] args) { new Thread(new Runnable() { public void run() { synchronized (lockA) { run = false; } } }).start(); while (true) { synchronized (lockB) { if (!run) break; } } } } 



How can it happen that the final fields change values?


One of the best examples of how final-field values ​​can change includes one specific implementation of the String class.

A string can be implemented as an object with three fields — an array of characters, an offset in this array, and a length. The reason for choosing a String implementation in this way instead of being implemented as a single char [] type field may be that it allows several strings and StringBuffer objects to share the same character array and avoid additional object allocation and memory copying.So, for example, the String.substring () method can be implemented by creating a new string that shares the same array of characters with the original string and is simply distinguished by the length and offset fields. String has all three fields - final.
Translator comment
update 6 JRE 7 Oracle java.lang.String
 public final class String implements Serializable, Comparable<String>, CharSequence { private final char[] value; private final int offset; private final int count; .... } 

update 6 JRE 7 Oracle java.lang.String ( offset count)
. , «» value « » ( ). :
1. null, value — null
2. null, value null, char[] value char-, 0-.

Translator comment
— String.substring(), char[] value
 public final class String implements Serializable, Comparable<String>, CharSequence { private final char[] value; private final int offset; private final int count; .... public String substring(int beginIndex, int endIndex) { .... return new String(offset + beginIndex, endIndex - beginIndex, value); } .... } 


— StringBuffer.toString(), String StringBuffer char[] value
 abstract class AbstractStringBuilder implements Appendable, CharSequence { char[] value; ... } public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequence { private final char[] value; private final int offset; private final int count; .... public synchronized String toString() { return new String(value, 0, count); } .... } 


 String s1 = "/usr/tmp"; String s2 = s1.substring(4); 


Line s2 will have an offset (offset) of 4 and a length (length) of 4. But within the old memory model, it was possible for another thread to see the default value of the offset (offset) (0), and later see the correct value of 4. It will seem as if the string "/ usr" has changed to "/ tmp".

The original memory model allowed this behavior and some JVMs demonstrated it. The new memory model prohibits it.

How do final-fields work with a new memory model?


Values ​​for the object's final-fields are specified in the constructor. If we assume that the object is built “correctly”, then as soon as the object is built, the values ​​assigned to the final-fields in the constructor will be visible to all other threads without synchronization. In addition, the visible values ​​for any other object or array referenced by these final-fields will be at least as “fresh” as the values ​​of the final-fields.
Translator comment
final- .
So
 public class App { final int k; public App10(int k) { this.k = k; } } 


 public class App { final int k; { this.k = 42; } } 


Translator comment
, ( instance — volatile, ). , "[1, 2]"
 import java.util.Arrays; public class App { final int[] data; public App() { this.data = new int[]{1, 2}; } static App instance; public static void main(String[] args) { new Thread(new Runnable() { public void run() { instance = new App(); } }).start(); while (instance == null) {/*NOP*/} System.out.println(Arrays.toString(instance.data)); } } 


, . , "[1, 0]" "[1, 2]" . , 1 final-.
 import java.util.Arrays; public class App { final int[] data; public App() { this.data = new int[]{1, 0}; this.data[1] = 2; } static App instance; public static void main(String[] args) { new Thread(new Runnable() { public void run() { instance = new App(); } }).start(); while (instance == null) {/*NOP*/} System.out.println(Arrays.toString(instance.data)); } } 


, . , "[1, 2]". 1 final-.
 import java.util.Arrays; public class App { final int[] data; public App() { int[] tmp = new int[]{1, 0}; tmp[1] = 2; this.data = tmp; } static App instance; public static void main(String[] args) { new Thread(new Runnable() { public void run() { instance = new App(); } }).start(); while (instance == null) {/*NOP*/} System.out.println(Arrays.toString(instance.data)); } } 



What does it mean for an object to be “properly built”? This simply means that the reference to the object “does not leak” until the end of the instance building process (see Safe Construction Techniques for examples).
Translator comment
( ): « » .

In other words, do not place a link to an object under construction at any place in which another thread can see it. Do not assign it to a static field, do not register the object as a listener in any other object, and so on. These tasks must be done upon completion of the constructor (outside the constructor, after its call), not in it.
 class FinalFieldExample { final int x; int y; static FinalFieldExample f; public FinalFieldExample() { x = 3; y = 4; } static void writer() { f = new FinalFieldExample(); } static void reader() { if (f != null) { int i = fx; int j = fy; } } } 

Translator comment
, reader() writer() « » .


This class is an example of how final-fields should be used. The thread calling the reader () method is guaranteed to read 3 into fx, since this is a final field. But there is no guarantee that it will read 4 in y, since it is a non-final-field. If the constructor for the FinalFieldExample class looked like this:
 public FinalFieldExample() { // bad! x = 3; y = 4; // bad construction - allowing this to escape global.obj = this; } 

then there is no guarantee that the thread reading the link to this object from global.obj will read 3 of x.

The ability to see a properly constructed value for a field is good, but if the field itself is a link, then you also want your code to see the “fresh” value in the referenced object (or array). You get this guarantee if your field is final. Thus, you can have a final-link to an array and not worry that other threads will see the correct value for the link to the array, but incorrect values ​​for the content of the array. Again, by “correct” here, we mean “at the time of the end of the object constructor”, and not “the last saved value”.

After all of the above, I want to make the remark that even after constructing an immutable object (an object containing only final-fields), if you want to make sure that all other threads see your link, you still need to use synchronization. There is no other way to ensure that a reference to an immutable object is visible in another thread. The guarantees received by your code from using final-fields should be deeply and carefully aligned with an understanding of how you deal with concurrency in your code.

If you use JNI to modify the final-field, then the behavior is undefined.
Translator comment
, Reflection API.


What does volatile do?


volatile-fields are special fields that are used to transfer state between threads. Each read from volatile will return the result of the last write by any other stream; in fact, they are specified by the programmer as fields for which it is not acceptable to see the “stale” value as a result of caching or reordering. The compiler and runtime environment are prohibited from placing them in registers. They also need to make sure that after writing to volatile, data is “pushed” (flushed) from the cache to main memory, so they are immediately visible to other threads. Similarly, before reading the volatile-field, the cache must be freed, so that we will see the value in the RAM, and not in the cache. There are also additional restrictions on changing the order in which volatile variables are accessed.

With the old memory model, access to volatile variables could not be reordered with each other, but they could be reordered with non-volatile variables. This negated the usefulness of volatile fields as a means of transmitting a signal from one stream to another.

According to the new memory model, it is still true that volatile variables cannot be reordered with each other. The difference is that now it’s not so easy to change the order between regular fields located alongside volatile. Writing to a volatile field has the same memory effect as a monitor release, and reading from a volatile field has the same memory effect as monitor acquire. In essence, since the new model imposes more stringent restrictions on the reordering between access to volatile fields and other fields (volatile or normal), everything that was visible to flow A when he wrote in volatile f becomes visible to flow B, when he will read f.
Translator comment
1 (data — volatile, run — volatile)
 public class App { static volatile int data = 0; static volatile boolean run = true; public static void main(String[] args) { new Thread(new Runnable() { public void run() { data = 1; run = false; } }).start(); while (run) {/*NOP*/}; System.out.println(data); } } 

. 1, 0 1 (data — volatile, run — volatile), -volatile «» volatile, —
 public class App { static int data = 0; static volatile boolean run = true; public static void main(String[] args) { new Thread(new Runnable() { public void run() { data = 1; run = false; } }).start(); while (run) {/*NOP*/}; System.out.println(data); } } 

(run — volatile «» ). , 1, 0 (data — volatile, run — volatile), -volatile
 public class App { static int data = 0; static boolean run = true; public static void main(String[] args) { new Thread(new Runnable() { public void run() { data = 1; run = false; } }).start(); while (run) {/*NOP*/}; System.out.println(data); } } 

. (run — volatile «» ). 1
 public class App { static int data = 0; static boolean run = true; public static void main(String[] args) { new Thread(new Runnable() { public void run() { data = 1; run = (data != 1); } }).start(); while (run) {/*NOP*/}; System.out.println(data); } } 

. , 1, 0 (data — volatile, run — volatile), -volatile «» volatile
 public class App { static volatile int data = 0; static boolean run = true; public static void main(String[] args) { new Thread(new Runnable() { public void run() { data = 1; run = false; } }).start(); while (run) {/*NOP*/}; System.out.println(data); } } 


Here is a simple example of how volatile fields can be used.
 class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { //uses x - guaranteed to see 42. } } } 

Let's call one thread a writer and the other a reader . Writing in v in the writer “flushes” x data into RAM, and reading v “captures” this value from memory. Thus, if the reader sees the true value of the v field, it is also guaranteed to see the value 42 in x. This was not true for the old memory model (in the old one, it was possible to “lower” the record in the non-volatile “below” record in the volatile). If v were not volatile, the compiler could change the writing order in the writer , and the reader could see 0 in x.
Translator comment
Joshua Bloch «Effective Java» 2nd edition (Item 66: Synchronize access to shared mutable data) «Java. » 1 ( 48. )


The semantics of volatile has been significantly enhanced, almost to the synchronized level. Each volatile read or write acts as a “half” synchronized in terms of visibility.

Important note: Please note that it is important that both threads do a read-write on the same volatile variable in order to achieve a relationship happens before. This is not the case when all that is visible to the stream A, when he writes a volatile-field f becomes visible to the stream B after he considers the volatile-field g. Reading and writing must refer to the same volatile variable in order to have proper semantics.

Has the new memory model "double-checked locking" solved the problem?


The (infamous) double-checked locking idiom (also called the multithreaded singleton pattern) is a trick designed to support lazy initialization in the absence of synchronization overhead. In the earliest JVMs, synchronization was slow and the developers sought to remove it, perhaps too zealously. Double-checked locking idiom looks like this:
 // double-checked-locking - don't do this! private static Something instance = null; public Something getInstance() { if (instance == null) { synchronized (this) { if (instance == null) instance = new Something(); } } return instance; } 

Translator comment
( , « » getInstance() — , this)
 // double-checked-locking - don't do this! public class Something { private static Something instance = null; public static Something getInstance() { if (instance == null) { synchronized (Something.class) { if (instance == null) instance = new Something(); } } return instance; } .... } 

Private Mutex
 // double-checked-locking - don't do this! public class Something { private static final Object lock = new Object(); private static Something instance = null; public static Something getInstance() { if (instance == null) { synchronized (lock) { if (instance == null) instance = new Something(); } } return instance; } .... } 



It looks awfully clever - we avoid syncing on the most frequent execution path. There is only one problem with this - the idiom does not work. Why?The most obvious reason is that the data record initializing the instance and writing the reference to the instance in a static field can be reordered by the compiler or cache, which will have the effect of returning something “partially constructed”. The result will be that we read an uninitialized object. There are many other reasons why both this idiom and algorithmic corrections to it are incorrect. There is no way to fix this in the old java memory model. More information can be found in “Double-checked locking: Clever, but broken” and then “The 'Double Checked Locking is broken' declaration” .

, volatile , double-checked-locking. 1.5, volatile . , volatile «» double-checked-locking, « » (happens before) Something .

However, for lovers of double-checked locking (and we really hope that they are not left), the news is still not very good. The whole point of double-checked locking was to avoid synchronization overhead. Not only is the short-term synchronization now MUCH cheaper than in Java 1.0, but also in the new memory model, the drop in performance when using volatile almost reached the level of synchronization cost. So there is still no good reason to use double-checked locking. Edited: volatile is cheap on most platforms.

Instead, use the Initialization On Demand Holder idiom, which is safe and much easier to understand:
 private static class LazySomethingHolder { public static Something something = new Something(); } public static Something getInstance() { return LazySomethingHolder.something; } 

This code is guaranteed to be correct, which follows from the initialization guarantees for static fields; if the field is set in a static initializer, it is guaranteed to make it correctly visible, for any stream that refers to this class.
Translator comment

 public class Something { private static Something instance = null; public static Something getInstance() { return LazySomethingHolder.something; } .... private static class LazySomethingHolder { public static Something something = new Something(); } } 


  • LazySomethingHolder.something
  • «»

( «12.4.1. When Initialization Occurs» ):
A class or interface type T will be initialized immediately before the first occurrence of any one of the following:
  • ...
  • A static field declared by T is assigned.
  • ...

A class or interface will not be initialized under any other circumstance.

«12.4.2. Detailed Initialization Procedure» .


What if I write a virtual machine?


You should look at http://gee.cs.oswego.edu/dl/jmm/cookbook.html .

Why should I worry?


? . , , . , , ; , , .


Contacts



I do Java online training (here are the programming courses ) and publish some of the training materials as part of the reworking of the Java Core course . You can see videos of lectures in the audience on the youtube channel , perhaps the video of the channel is better organized in this article .

skype: GolovachCourses
email: GolovachCourses@gmail.com

Source: https://habr.com/ru/post/221133/


All Articles