Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

What is the JAVA memory model?

2025-04-02 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

This article introduces what the JAVA memory model is like, the content is very detailed, interested friends can refer to, hope to be helpful to you.

The concurrent three questions section introduces the knowledge of reordering, memory visibility, and atomicity, which are why concurrent programs are difficult to write.

Reorder readers to run the following programs on their computers first:

Public class Test {

Private static int x = 0, y = 0 new CountDownLatch private static int a = 0, b = 0 * * public static void main (String [] private) throws InterruptedException {private I = 0; for (;;) {iPrefecture; x = 0; y = 0; a = 0; b = 0; CountDownLatch latch = new CountDownLatch (1); Thread one = new Thread (()-> {try {private () } catch (InterruptedException e) {} a = 1; x = b;}); Thread other = new Thread (()-> {try {latch.await ();} catch (InterruptedException e) {} b = 1; y = a }); one.start (); other.start (); latch.countDown (); one.join (); other.join (); String result = "th" + I + "(" + x + "," + y + ")"; if (x = = 0 & & y = = 0) {System.err.println (result); break } else {System.out.println (result);}

} after a few seconds, we can get the result x = = 0 & & y = = 0. If you look closely at the code, you will see that this result would not have happened without reordering.

Reordering is caused by the following mechanisms:

Compiler optimization: for operations that do not have data dependencies, the compiler will rearrange to a certain extent during compilation.

If you take a closer look at the code in thread 1, the compiler can change the order of a = 1 and x = b, because there is no data dependency between them, and the same is true for thread 2, then it is not difficult to get the result of x = y = 0.

Instruction reordering: CPU optimizes behavior, which also rearranges instructions that do not have data dependencies to a certain extent.

This is similar to compiler optimization, even if the compiler does not rearrange, CPU can rearrange instructions, needless to say.

Memory system reordering: the memory system does not reorder, but due to the existence of cache, the program as a whole will show out-of-order behavior.

Assuming that there is no compiler rearrangement and instruction rearrangement, thread 1 modifies the value of a, but after the modification, the value of a may not have been written back to main memory, so it is natural for thread 2 to get a = = 0. Similarly, thread 2's assignment to b may not be flushed to main memory in time.

Memory visibility when talking about reordering, we also talked about the issue of memory visibility. Let's talk a little bit more here.

The problem of visibility of shared variables between threads is not directly caused by multicore, but by multiple caches. If each core shares the same cache, there is no memory visibility problem.

In modern multicore CPU, each core has its own first-level cache or first-level cache plus second-level cache, and so on. The problem occurs in the exclusive cache of each core. Each core reads the data it needs into the exclusive cache, writes the modified data to the cache, and then waits for it to be brushed into main memory. So it causes some cores to read a value that is out of date.

Java, as a high-level language, shields these low-level details and defines a set of specifications for reading and writing memory data with JMM. Although we no longer need to care about the first-level cache and the second-level cache, JMM abstracts the concept of main memory and local memory.

All shared variables exist in main memory, each thread has its own local memory, and threads read and write shared data are exchanged through local memory, so the visibility problem still exists. The local memory here is not really a piece of memory allocated to each thread, but an abstraction of JMM, an abstraction of registers, first-level cache, second-level cache, and so on.

Atomicity in this article, atomicity is not the focus, it will be introduced as a part of concurrent programming to consider.

When it comes to atomicity, you should all think of long and double, whose values require 64-bit memory. According to the Java programming language specification, the writing of 64-bit values can be divided into two 32-bit operations. Originally, the whole assignment operation is divided into two operations: low 32-bit assignment and high 32-bit assignment. If other threads read this value, it is bound to read a strange value.

At this point we are going to use the volatile keyword for control, and JMM specifies that the atomicity of write operations needs to be guaranteed for volatile long and volatile double,JVM.

In addition, reading and writing to references is always atomic, whether it's a 32-bit machine or a 64-bit machine.

The Java programming language specification also mentions that developers of JVM are encouraged to ensure the atomicity of 64-bit value operations and that users are encouraged to use volatile or the correct synchronization methods as much as possible. The key word is "encourage".

In 64-bit JVM, it is possible not to add volatile, which can also guarantee atomicity for long and double write operations. On this point, I have not found any official material to describe it. If readers have any relevant information, I hope you can give me some feedback.

Java's concurrency constraints on concurrency make it possible for our code to produce a variety of execution results, which is obviously unacceptable, so the Java programming language specification needs to stipulate some basic rules. JVM implementers will implement JVM under the constraints of these rules, and then developers have to write code according to the rules, so that we can accurately predict the execution results. Here are some brief introductions.

The Synchronization Order Java language specification defines a series of rules for synchronization: 17.4.4. Synchronization Order, which includes the following synchronization relationships:

Unlocking for monitor m and locking synchronization for m for all subsequent operations

Writing to the volatile variable v, synchronized with subsequent reads of v by all other threads

The operation of starting the thread is synchronized with the first operation in the thread.

For each attribute, the write default value (0, false,null) is synchronized with what each thread does to it.

Although it is a bit strange to write default values for object properties before object creation is complete, conceptually, each object is initialized with default values when the program starts.

The last operation of thread T1 and thread T2 found that thread T1 had ended synchronization.

Thread T2 can determine whether T1 has been terminated by the T1.isAlive () or T1.join () method.

If thread T1 interrupts T2, the interrupt operation of thread T1 is found to be out of sync with all other threads (by throwing an InterruptedException exception, or by calling Thread.interrupted or Thread.isInterrupted)

Happens-before Order two operations can use happens-before to determine the order in which they are executed. If one operation happens-before the other, then we say that the first operation is visible to the second operation.

If we have operation x and operation y respectively, we write hb (x, y) to represent x happens-before y. The following rules also come from the Java 8 language specification Happens-before Order:

If operation x and operation y are two operations of the same thread, and operation x precedes operation y in code, then there is hb (x, y).

The last line of instructions for the object constructor happens-before the first line of the finalize () method.

If operation x is synchronized with the subsequent operation y, then hb (x, y). This one is about the previous section. Hb (x, y) and hb (y, z), then it can be inferred that hb (x, z) mentioned here, x happens-before y, does not mean that the x operation must be performed before the y operation, but that the execution result of x is visible to y, as long as the visibility is satisfied, reordering can occur.

Synchronized keyword monitor, here translated into monitor lock, for everyone's understanding convenience.

The keyword synchronized has been used a lot. I won't teach you how to use it here. Let's take a look at its impact on memory visibility.

After acquiring the monitor lock, a thread can enter the code block controlled by synchronized. Once it enters the code block, first of all, the thread's cache of shared variables will be invalidated, so the reading of shared variables in the synchronized code block needs to be re-obtained from the main memory, and the latest values can be obtained.

When exiting the code block, the data in the thread's write buffer is brushed into main memory, so operations on shared variables before the synchronized code block or in the synchronized code block are immediately visible to other threads as the thread exits the synchronized block (provided that other threads reading the shared variables read the latest values from main memory).

Therefore, we can summarize that thread an is visible to subsequent threads holding the same monitor lock for operations on shared variables before entering the synchronized block or in synchronized. Although it is a very simple sentence, please feel it.

Note that when entering synchronized, there is no guarantee that the previous write operations will be flushed into the main memory. Synchronized mainly ensures that the data from the local memory will be brushed into the main memory when exiting.

Double check in singleton mode We strike while the iron is hot to solve the problem of double check in singleton mode. On this issue, the great gods have sent an article to elaborate on this, here to move.

Come and pay homage to the great gods in the signature of the next article: David Bacon (IBM Research) Joshua Bloch (Javasoft), Jeff Bogda, Cliff Click (Hotspot JVM project), Paul Haahr, Doug Lea, Tom May, Jan-Willem Maessen, Jeremy Manson, John D. Mitchell (jGuru) Kelvin Nilsen, Bill Pugh, Emin Gun Sirer, at least Joshua Bloch and Doug Lea are no strangers.

Cut the crap and look at the following singleton pattern:

Public class Singleton {

Private static Singleton instance = null;private int Vprivate Singleton () {this.v = 3;} public static Singleton getInstance () {if (instance = = null) {/ / 1. Check synchronized (Singleton.class) {/ / 2 if (instance = = null) {/ / 3 for the first time. Check instance = new Singleton (); / / 4} return instance;} for the second time

Many people know that the above way of writing is wrong, but they may not be able to say why it is wrong.

Let's assume that two threads an and b call the getInstance () method. Suppose a goes first, goes all the way to 4, and executes the code instance = new Singleton ().

The code instance = new Singleton () first applies for a space, then initializes each property to zero (0/null), performs the attribute assignment in the constructor [1], and assigns a reference to the object to instance [2]. During this process, reordering may occur in [1] and [2].

At this point, as soon as thread b comes in and executes to 1 (see the code block above), it is possible to see that instance is not null, and then thread b will not wait for the monitor lock, but will return instance directly. The problem is that the instance may not have finished executing the constructor (thread an is still at step 4 at this time), so the instance obtained by thread b is incomplete, and the attribute value in it may be the initialized zero (0/false/null), rather than the value specified by thread an in the constructor.

Review the previous knowledge and analyze why there is this problem here.

1. The compiler can inline the constructor, and then reorder will be easy to understand.

2. Even if no code reordering occurs, the assignment of the attribute by thread an is written to the local memory of thread a, which is not visible to thread b.

Finally, if thread a comes out of the synchronized block, then instance must be a complete instance of the correct construction, which is the memory visibility guarantee of synchronized as we mentioned earlier.

-dividing line-

For most readers, this section is actually over, and as many readers know, the solution is to use the volatile keyword, which we'll talk about when we introduce volatile. Of course, if you have patience, you can continue to take a look at this section.

Let's take a look at the following code to see if it can solve the problem we encountered before.

Public static Singleton getInstance () {if (instance = = null) {/ / Singleton temp; synchronized (Singleton.class) {/ / temp = instance; if (temp = = null) {/ / synchronized (Singleton.class) {/ / embedded a synchronized block temp = new Singleton ();} instance = temp; / /} return instance } the above code is very interesting and wants to take advantage of synchronized's memory visibility semantics, but this solution still fails, let's analyze it.

As we said earlier, when synchronized exits, it is guaranteed that writes to shared variables in the synchronized block will be flushed into main memory. In other words, in the above code, when the embedded synchronized ends, the temp must be fully constructed, and then the value assigned to the instance must be good.

However, synchronized ensures that the code before releasing the monitor lock must be executed before releasing the lock (for example, initialization of temp must be performed before releasing the lock), but there is no rule that code after releasing the lock cannot be executed before releasing the lock.

That is, the behavior instance = temp in the code after the lock is released can be executed in advance to the previous block of synchronized code, and the reordering problem comes up again.

Finally, if all the attributes are modified with final, the double checking described earlier is feasible and does not need to add volatile. This will be introduced in the final section.

Volatile keyword most developers should know how to use this keyword, but they may not know much about why.

If someone asks you the role of volatile in your next interview, remember two things: memory visibility and forbidding instruction reordering.

We still use JMM's main memory and local memory abstractions to describe the memory visibility of volatile, which is more accurate. Also, Java is not the only language that has the volatile keyword, so the following description must be based on the environment where Java abstracts out the memory model after being cross-platform.

Remember the semantics of synchronized? When entering synchronized, the local cache is invalidated, and the reading of shared variables in the synchronized block must be read from the main memory; when exiting the synchronized, the writes before entering the synchronized block and in the synchronized block will be brushed into the main memory.

Volatile has a similar semantics. Before reading a volatile variable, you need to invalidate the corresponding local cache, so you must read the latest value to the main memory. Writing a volatile property will immediately be brushed into the main memory. Therefore, volatile read and monitorenter have the same semantics, volatile write and monitorexit have the same semantics.

Volatile forbids reordering you remember the previous singleton mode of double checking. As mentioned earlier, adding a volatile can solve the problem. In fact, it takes advantage of volatile's prohibition of reordering.

Volatile's prohibition of reordering is not limited to two volatile attribute operations that cannot be reordered, but also volatile property operations and normal attribute operations around it.

Previously, in instance = new Singleton (), if instance is volatile, then the assignment to instance (assigning a reference to the instance variable) will not be reordered with the attribute assignment in the constructor, ensuring that this object reference will not be assigned to instance until the constructor finishes.

Based on the memory visibility of volatile and the prohibition of reordering, it is not difficult to draw a corollary: if thread a writes a volatile variable and thread b reads it again, then all properties visible to thread an are visible to thread b.

The volatile summary volatile modifier applies to scenarios where an attribute is shared by multiple threads, one of which modifies the property, and other threads can get the modified value immediately. It is used a lot in the source code of concurrent packages. The read and write operations of the volatile attribute are unlocked, and it is not a substitute for synchronized because it does not provide atomicity and mutex. Because there is no lock, it does not take time to acquire and release the lock, so it is low-cost. Volatile can only work on attributes, and we decorate the property with volatile so that compilers does not reorder the property. Volatile provides visibility, and any changes made by one thread will be immediately visible to other threads. The volatile property is not cached by the thread and is always read from main memory. Volatile provides happens-before guarantees for writing to the volatile variable v and subsequent read operations to v by all other threads. Volatile can make the assignment of long and double atomic, as mentioned earlier when talking about atomicity. Classes decorated with the final keyword final cannot be inherited, methods decorated with final cannot be overridden, and attributes decorated with final cannot be modified once initialized. Of course, we don't care about these jokes. In this section, let's take a look at the memory visibility impact of final.

When talking about the singleton mode of double checking, I mentioned that if all attributes are modified with final, then volatile can be avoided, which is the visibility impact of final.

Set the final property in the constructor of the object, and do not write a reference to the object where other threads can access it until the object is initialized (do not let the reference escape in the constructor). If this condition is met, when other threads see the object, that thread can always see the final property of the correctly initialized object.

As stated above, it is clear that the write of the final property will not be reordered with the assignment of this reference, such as:

X.finalField = v;...; sharedRef = x; if you want to see more introduction to final, you can move to the semantic section of the final attribute of the Java language specification that I translated earlier.

About how the JAVA memory model is shared here, I hope the above content can be of some help to you, can learn more knowledge. If you think the article is good, you can share it for more people to see.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report