Talking about thread safety from the JVM memory model

Source: Internet
Author: User
Tags prepare

As an independent developer who didn't go to work for three months, I went to Xiaomi for an interview today. How to say, regardless of your level, please make sure to prepare before the interview, as one of the interviewer said, I know you are good, but in any case to prepare the next, How else could you have forgotten this method?

At the moment, I suddenly feel that I am a fake programmer. Why, as a programmer who has been writing code since the 12, it is shameful to forget a method. To write an article is called "I'm a fake programmer" to talk about these interesting things.

Not much to say, the topic to talk about today is relatively deep, broader, but I try to make him look clear. Memory Hierarchy

For developers, the memory hierarchy should be very familiar, roughly as follows:


Where registers, l1,l2,l3 are encapsulated in CPU chips, we seldom pay attention and use it as an application developer. The introduction of the L1,L2,L3 high-speed register is essentially to address the access operator and memory speed mismatch. But the introduction of caching also brings two issues: Cache Hit rate: Cached data is a backup of the data in main memory, if the data required by the instruction is in the cache, we say that the cache hit, and vice versa, need to be obtained from main memory. A good caching strategy should be as high as possible, and how to improve it is a very difficult thing. Cache consistency Problem: We know that the cache is a backup of main memory data, but each core has its own cache, when the data in the cache and in-memory data inconsistency, should be based on whose data, which is the so-called cache consistency problem.

The above is just showing the memory hierarchy, and now let's look more closely at the CPU chip-to-memory connection, with Intel i5 dual-Core processors as an example:


With the above figure we can clearly see the connection between the various caches, in the subsequent JVM memory model anatomy, you will also find similar structure. About the memory hierarchy here is enough, after all, we do not specialize in operating systems, let's talk about main memory, more specifically, the abstract virtual RAM. Virtual Memory

When talking about memory, everyone's mind will appear in the memory of the image, in many cases, this kind gives us the most intuitive understanding of memory, for non-developers so understanding is acceptable, but for the engineers engaged in development and development work, we have to deepen a little.


From a hardware point of view, memory is a fixed capacity of the storage body, and the hardware directly dealing with our operating system. We know that the process of the system is to share the CPU and memory resources, modern operating system in order to more efficient management of memory, put forward the abstract memory concept, called virtual memory. In other words, The memory management we talked about in the operating system is all about virtual memory. Virtual memory brings several benefits: virtual memory is considered to be a telling cache of the address space stored on disk. Before the application is run, it is only stored on disk binaries, and after running, the app is copied into main memory. It provides a consistent address space for each process, simplifying the memory management mechanism. It's easy to see that each process considers itself exclusive of the main memory. The simplest example is that a building is divided into multiple rooms, each room is independent, and the owner is exclusive, and each owner can self-decorate from scratch. In addition, You will not be able to enter other rooms without the consent of the other head of household.

The presentation of virtual memory also alters the way memory is accessed. Previous CPU accesses main memory in the following way:


The diagram above shows the process by which the CPU accesses main memory directly through the physical address (assuming 2), but if there is virtual memory, the entire access process is as follows:


The CPU is given a virtual address, and then the virtual address is translated into a real physical address by the MMU (Memory management Unit, hardware), and then the main memory is accessed. For example, now the virtual address is 4200 through the MMU translation directly into the real physical address 2.

Here's what the virtual memory address is. We know that virtual memory provides an illusion for each process: each process is exclusively using main memory, and each process sees the same amount of RAM, which is called the virtual address space. For example, for instance, our memory bar is 1G, which is the maximum address space of 210, At this point a process requires 4G of memory, then the operating system can map it into a larger address space 232, the address space is called virtual memory address. about how to map, interested can learn by themselves. An abstract representation using a graph:


By now we know that the memory we talked about in the operating system is actually virtual memory, and if you're a C-language developer, that might feel a little deeper. Now that each process has its own virtual address space, how does it work? For example, for Linux systems, Take a look at the layout of its process space address:


So far, we've come to the process. We know that each JVM runs in a separate process, unlike a normal application, which is the equivalent of an operating system that has its own memory model. Below, plunge into the JVM's memory model. concurrency Model (threads)

If Java does not have multi-threading support and there is no JIT present, then there will be no JVM memory model. Why do you say that? First of all, we start with the JIT, the JIT will track the process of running the program, and where possible to optimize, which has an optimization and the processor of the chaotic execution of similar, But this is called the command rearrangement. If there is no multi-threading, there will be no so-called critical resources, if this precondition does not exist, of course, there will be no competition for resources. This way, perhaps Java has been abandoned in the long river of history.

Although the Java language does not work directly with memory like the C language, mastering the JVM memory model is still important. For why mastering the JVM memory model starts with the Java Concurrency programming model.

There are two key issues that need to be addressed in the concurrency model: How to communicate between threads and how threads are synchronized. So-called communication refers to how messages are exchanged between threads, and synchronization is used to control the relative order in which operations occur between different threads.

From the perspective of implementation, concurrency models generally have two ways: based on shared memory and message-based delivery. The difference between the two implementations determines the differences in the behavior of communication and synchronization. In the concurrency model based on shared memory, the synchronization is displayed, the communication is implicit, and in the message-passing concurrency model, the communication is explicit. Synchronization is implicit. Let's explain it in detail.

In the concurrency model of shared memory, any thread can operate in public memory, and if synchronization is not displayed, then the order of execution is not known, or because the thread can operate on public memory, so communication is implicit. In a message-passing concurrency model, the message must be sent before it is accepted. , so synchronization is implicit, but the threads must communicate by explicitly sending messages.

In the final concurrency model selection scenario, Java chooses a concurrency model based on shared memory, that is, explicit synchronization, implicit communication. If you do not handle these two problems when writing programs, there are a variety of strange problems in multithreading. So, for any Java programmer, It is important to be familiar with the JVM's memory model. JVM Memory Structure

For JVM memory, there are two main aspects: the JVM memory structure and the JVM memory model. The difference between the two is that the model is a protocol that specifies that the read and write process for a particular memory or cache should never be confused.

Many people tend to be confused about the structure of the JVM's memory and the memory structure of the process, and here I will help you comb it.

The JVM is essentially a program, but it has a similar operating system feature. When a JVM instance starts running, in the Linux process, the memory layout is as follows:


The JVM is divided again on the basis of the process heap space to take a simple look. The eternal life is essentially the code and data area of the Java program, and the young generation and the old are the real heap areas that the Java program uses, That's what we often say. But the heap and the heap at this point are very different: when you call the malloc function of a C program, it causes a system-level call, and when you use the free function to release memory, it also causes a system-level call, but the heap area in the JVM is not: The JVM applies a single piece of contiguous memory to the system as a heap of Java programs, and when the Java program uses new to request memory, the JVM assigns it to the memory area as needed. Without the need for a system-level call. It can be seen that the JVM actually implements a heap memory management mechanism, which has the following advantages: reducing system-level calls. Most memory applications and looking back don't need to trigger system functions, Calls to system functions are only caused by changes in the Java heap size. The JVM implements lower memory management costs than system-level calls. Reduce the occurrence of memory leaks. With the JVM taking over the memory management process, you can avoid memory leaks in most cases.

The JVM memory structure is now briefly introduced, and hopefully this will help you get through the top and bottom. Of course, for the sake of understanding, I omitted some of the relatively unimportant points, if interested can learn by themselves. After the JVM memory structure is finished, what is the next step? JVM Memory Model

Java employs a concurrency model based on shared memory, making the JVM look very similar to modern multicore processors: In a multi-core processor architecture based on shared memory, each processor has its own cache and is regularly reconciled with the main memory. The thread here also has its own cache (also called working memory), at which point, The JVM memory model presents the following structure:


The image above shows the memory model of the JVM, also known as JMM. For JMM There are the following provisions: 1. All variables are stored in main memory (main memories) 2. Each thread also has its own working memory 3. A variable in working memory is a copy of the main memory variable, and the thread cannot read and write directly to the main memory variable, but only the variable 4 in its own working memory. The working memory is not shared between threads, and if communication between threads needs to be done by using main memory

The memory area in which the shared variable resides is the shared memory, also called the heap memory, where variables are shared and accessed by multiple threads. The more popular point is that in Java, heap memory is shared between threads, while local variables, formal parameters, and exception program parameters are not in heap memory. Therefore, there is no multi-threaded sharing scenario.

In contrast to the JMM rule, we define the following four atomic operations to implement the process of copying a variable from main memory to working memory: read: Reads the main memory variable and transfers it to the working memory load: The value of the variable obtained from the main memory in the read operation is placed in the copy of the working memory store: The value of a variable in the working memory is transferred to the main memory for use in the following write operation write: Put the store operation from the value of the variable obtained in the working memory into the variable of main memory.

As you can see, the process from main memory to working memory is actually going through read and load two operations, instead of going through store and write two operations.

Now let's take a look at a piece of code and talk about the multithreading security issue with the above:

public class ThreadTest {public static void main (string[] args) throws Interruptedexception {Sharevar ins =
        New Sharevar ();

        list<thread> threadlist = new arraylist<> ();
            for (int i = 0; i <; i++) {thread thread;

            if (i% 2 = = 0) {thread = new thread (new Addthread (INS));

            } else {thread = new thread (new Subthread (INS));
            } thread.start ();

        Threadlist.add (thread);
        } for (Thread thread:threadlist) {thread.join ();

    } System.out.println (Thread.CurrentThread (). GetId () + "" + Ins.getcount ());

    }} class Sharevar {private int count; public void Add () {try {thread.sleep (100);//here to better reflect multithreading security problems} catch (Interruptedexception E
        ) {e.printstacktrace ();

    } count++;
    } public void sub () {count--;

    }public int GetCount () {return count;

    }} class Addthread implements Runnable {private Sharevar sharevar;
    Public Addthread (Sharevar sharevar) {this.sharevar = Sharevar;
    } @Override public void Run () {sharevar.add ();

    }} class Subthread implements Runnable {private Sharevar sharevar;
    Public Subthread (Sharevar sharevar) {this.sharevar = Sharevar;
    } @Override public void Run () {sharevar.sub (); }
}

Ideally, you should output 0 at a time, but you might output-1 or 2-if you run it multiple times. Why?

In the 10 threads created, each thread has its own working memory, and these threads share the count variable of the Sharevar object, and when the thread starts, it goes through the read-load operation to copy the variable from main memory to its own working memory. Each thread then operates the copy of the variable in its own working memory, and finally the copy is re-written to the main memory, replacing the value of the original variable. But in multiple threads, but because the threads cannot communicate directly, this leads to the variable changes can not be timely response to the thread, This subtle difference in time results in the value of the variable currently being manipulated by each thread is not necessarily up to date, which is called memory invisibility.

Now I think you have fully understood the origin of multithreading security. So how do you solve it? The simplest approach is to have multiple threads program serial read and write operations on shared objects, that is, only one thread is allowed to operate on the shared object at the same time. We make this mechanism a locking mechanism, In Java, each object has a lock, called a monitor, someone also called an object lock, at the same time, the object lock can only serve one thread.

With the lock object, how does it work? Two atomic operations are defined for this JMM: Lock: Identifies a variable of main memory as a thread exclusive state unlock: Unlocks the thread exclusive state of a variable in the main memory

In the lock object and the two atomic operations together, the locking mechanism can be synchronized, embodied in the language level is the Synchronized keyword. We also said that Java employs a concurrency model based on shared memory, which is typically characterized by explicit synchronization. That is, in order to manually use the Synchronized keyword to do the synchronization. Now let's improve the code above, just add the syhcronized keyword to the Add () and sub () methods, but before that, take a look at the corresponding bytecode files for both methods:

  public void Add ();                  Descriptor: () V flags:acc_public code:stack=3, locals=2, args_size=1 0:ldc2_w #2           Long 100l 3:invokestatic #4//Method Java/lang/thread.sleep: (J) V 6:goto 9:astore_1 10:aload_1 11:invokevirtual #6//Method java/lang/in Terruptedexception.printstacktrace: () V 14:aload_0 15:dup 16:getfield #7/
        /Field Count:i
        19:iconst_1 20:iadd 21:putfield #7//Field count:i
    24:return public void Sub ();
         Descriptor: () V flags:acc_public code:stack=3, Locals=1, args_size=1 0:aload_0 1:dup
         2:getfield #7//Field count:i
        5:iconst_1 6:isub 7:putfield #7//Field count:i 10:return linenumbertable: Line 18:0 19:10 

Now we use synchronized to make the two methods safe:

Class Sharevar {
    private int count;

    Public synchronized void Add () {
        try {
            thread.sleep;
        } catch (Interruptedexception e) {
            E.printstacktrace ();
        }
        count++;

    }

    Public synchronized void Sub () {
        count--;
    }

    

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.