What exactly is an in-memory model?

In multiprocessing systems, each CPU typically contains one or more layers of memory caches, which are designed to speed up data access (because the data is closer to the processor) and to reduce traffic on the shared memory bus (because many memory operations can be accommodated) to improve performance. Memory caching can greatly improve performance.

But at the same time, this design approach also brings many challenges.

For example, what happens when two cpus operate on the same memory location? Under what circumstances do the two cpus see the same memory value?

Now, the memory model comes into play!! At the processor level, the memory model explicitly defines how writes by other processors remain visible to the current processor, and how values written to memory by the current processor are visible to other processors. This feature is called visibility, which is an official definition.

However, visibility is also divided into strong visibility and weak visibility. Strong visibility means that any CPU can see the same value at a given memory location. Weak visibility means that you need special instructions called memory barriers to flush the cache or invalidate the local processor cache in order to see what other cpus have written to a given memory location. These particular memory barriers are wrapped, and we don’t know the concept of memory barriers until we study the source code.

Another feature of the memory model that allows compilers to reorder code (and reorder isn’t just a feature of compilers) is called orderliness. If two lines of code are unrelated to each other, the compiler can change the order in which they are compiled, as long as the code does not change the semantics of the program.

As we mentioned earlier, reordering is not only a compiler feature, it is a static reordering by the compiler. Reordering can also occur at runtime or during hardware execution. Reordering is a way to improve the efficiency of a program.

Take this code for example

Class Reordering {
  int x = 0, y = 0;
  public void writer(a) {
    x = 1;
    y = 2;
  }

  public void reader(a) {
    int r1 = y;
    intr2 = x; }}Copy the code

When two threads execute the above code in parallel, reordering may occur, because x and y are two unrelated variables. Therefore, when thread executes writer, reordering occurs. Y = 2 is compiled first, and then the thread switches to perform r1 write, followed by R2 write. Note that the value of x is 0 at this point, because x = 1 is not compiled. X = 1, r1 = 2, r2 = 0. This is the result of reordering.

So what does the Java memory model bring us?

The Java memory model describes what behaviors are legal in multiple threads and how threads interact with each other through memory. The Java memory model provides two features, visibility and order between variables, that we need to pay attention to in our daily development. Java also provides keywords such as volatile, final, and synchronized to help with the Java memory model, which defines the behavior of volatile and synchronized.

Will other languages, such as C++, have memory models?

Other languages such as C and C++ are not designed to support multithreading directly, and the reordering of compilers and hardware in these languages is guaranteed by thread libraries (such as PThreads), the compiler used, and the platform on which the code is run.

What is JSR-133 about?

In 1997, several serious flaws were discovered in the memory model in this version of Java, which often caused weird problems, such as changing field values, and easily weakened the compiler’s ability to optimize.

As a result, Java came up with an ambitious vision: merging the memory model, the first attempt by a programming language specification to merge a memory model that could provide consistent semantics for concurrency across architectures, but was much more difficult to implement than it seemed.

Finally, JSR-133 defines a new memory model for the Java language that fixes the flaws of earlier memory models.

So, jSR-133 is a specification and definition of the memory model.

The main design objectives of JSR-133 include:

  • Preserve Java’s existing security guarantees, such as type-safety, and enforce other security guarantees, such as the fact that every variable value observed by a thread must have been modified by a thread.
  • The synchronization semantics of your program should be as simple and intuitive as possible.
  • Leave the details of how multiple threads interact to the programmer.
  • Design correct, high-performance JVM implementations on a wide range of popular hardware architectures.
  • The guarantee of initialization safety should be provided so that if an object is constructed correctly, all threads that see the object’s construction can see the value of its final field set in the constructor without any synchronization.
  • The impact on existing code should be minimal.

What is reordering?

In many cases, access to program variables, such as object instance fields, class static fields, and array elements, is executed in a different order from the order specified by a programmer’s program. The compiler can arbitrarily adjust the order of instruction execution in the name of optimization. In this case, data can be moved between registers, processor cache, and memory in a different order than the program specifies.

There are many potential sources of reordering, such as compilers, JIT (just-in-time compilation), and caches.

Reordering is an illusion created by the hardware and the compiler. Reordering does not occur in single-threaded programs. It usually occurs in multithreaded programs that are not properly synchronized.

What was wrong with the old memory model?

The new memory model was proposed to compensate for the shortcomings of the old memory model, so I’m sure readers can guess roughly what the shortcomings of the old memory model are.

First, the old memory model did not allow reordering to occur. In addition, the old memory model did not guarantee the true immutability of final, which was a very shocking conclusion. The old memory model did not treat final differently from other fields that were not modified by final. This meant that strings were not truly immutable. This is a very serious problem indeed.

Second, the old memory model allowed volatile writes and non-volatile reads and writes to be reordered, which was at variance with most developers’ intuitions about volatile and caused confusion.

What is incorrect synchronization?

When we talk about incorrect synchronization, we mean any code

  • A thread writes to a variable,
  • Another thread reads the same variable,
  • And there is no proper synchronization between reads and writes

When these rules are violated, we say that a data race occurs on this variable. A program with data contention is an incorrectly synchronized program.

What does synchronization do?

Synchronization has several aspects. The most understandable is mutual exclusion, which means that only one thread can hold one monitor at a time. So synchronization on Monitor means that once a thread enters a block of synchronized code protected by Monitor, Other threads cannot enter the block protected by the Monitor until the first thread exits the synchronized code block.

Synchronization, however, is more than just mutually exclusive. It is also visible. Synchronization ensures that values written to memory by a thread are visible to other threads synchronizing on the same Monitor before the thread enters the synchronized code block and during the execution of the synchronized code block.

Before entering the synchronized block, monitor is retrieved, which has the effect of invalidating the local processor cache so that variables will be re-read from main memory. After exiting a synchronized code block, Monitor is released, which has the ability to flush the cache to main memory so that other threads can see the values written by the thread.

The semantics of the new memory model specify a sequence of memory operations (read, write, Lock, unlock) and thread operations (start, join) that ensures that the first action is visible to the second action before it is executed. This is the happens-before principle, and these particular orders are

  • Every action in the thread is happens-before before the thread action defined by the program.
  • Each unlock operation in Monitor happens-before the subsequent lock operation of the same Monitor.
  • Writes to volatile fields are happens-before before each subsequent read of the same volatile variable.
  • All start() calls to a thread are happens-before any action by the thread has been initiated.
  • All operations in a thread are happens-before any other thread returns successfully from join() on that thread.

It is important to note that synchronization between two threads in the same Monitor is very important. Not everything that is visible to thread A when it synchronizes on object X is visible to thread B when it synchronizes on object Y. Free and fetch must match (that is, perform on the same Monitor) to have correct memory semantics, otherwise data contention will occur.

Also, for information on the use of synchronized in Java, check out this article on synchronized!

How does final work under the new JMM?

As you already know, final doesn’t work under the old JMM, where the semantics of final are just like regular fields, but with the new JMM, the memory semantics of final have changed substantially. Let’s look at how final works under the new JMM.

The final fields of an object are set in the constructor, and once the object is correctly constructed, the final value in the constructor is visible to all other threads without synchronization.

What is correct construction?

Proper construction means that references to the object being constructed are not allowed to escape during construction, that is, do not place references to the object being constructed where another thread can see it. Here is an example of a correct construct:

class FinalFieldExample {
  final int x;
  int y;
  static FinalFieldExample f;
  public FinalFieldExample(a) {
    x = 3;
    y = 4;
  }

  static void writer(a) {
    f = new FinalFieldExample();
  }

  static void reader(a) {
    if(f ! =null) {
      int i = f.x;
      intj = f.y; }}}Copy the code

The thread executing the reader must see the value 3 of f.x because it is final. I’m not guaranteed to see a y value of 4 because it’s not final. If the FinalFieldExample constructor looks like this:

public FinalFieldExample(a) { 
  x = 3;
  y = 4;
  // False construction, escape may occur
  global.obj = this;
}
Copy the code

So there’s no guarantee that the value of x that I read is 3.

That is, if after one thread constructs an immutable object (that is, an object that contains only final fields), you want to ensure that it is seen correctly by all other threads, you usually still need to use synchronization correctly.

What did Volatile do?

I wrote a detailed article on the use and implementation of volatile, which you can read

Does the new memory model fix double-checked locks?

Perhaps all of us have seen the multithreaded singleton double-checked lock written as a technique to support lazy initialization while avoiding synchronization overhead.

class DoubleCheckSync{
 	private static DoubleCheckSync instance = null;
  public DoubleCheckSync getInstance(a) {
    if (instance == null) {
      synchronized (this) {
        if (instance == null)
          instance = newDoubleCheckSync(); }}returninstance; }}Copy the code

Such code looks clever in terms of the order in which the program is defined, but this code has a fatal problem: it doesn’t work.

??????

Double check lock not working?

Yes!

For the hair?

The reason is that writes to an initialized instance and writes to an instance field can be reordered by the compiler or the cache, so it looks like we might be reading an initialized instance, but you might actually be reading an uninitialized instance.

Many people thought that volatile would solve this problem, but in JVMS prior to 1.5, volatile was not guaranteed. Under the new memory model, using volatile fixes the problem of double-checked locking because there will be a happens-before relationship between the constructor thread initializing DoubleCheckSync and returning its value to the thread that reads it.

In addition, I have uploaded six PDFS by myself, with over 10W + spread across the Internet. After searching the public account of “Programmer Cxuan” on wechat, I reply to CXuan in the background and get all PDFS. These PDFS are as follows

Six free PDFS