Java memory model (C) atoms, memory visibility, reordering, sequential consistency, volatile, lock, final

 
 
 

A atomicity

Atomicity refers to the operation of a single indivisible operation corresponding to the operation. For example, count execution count ++ d atomic operation is not the operation of the variable int. Because the count ++ may actually be divided into three operations: (1) read the current value of the variable count; (2) to take count of the current value and an adder to do; (3) After the addition of the value of the count variable is assigned.

In a multithreaded environment, non-atomic operations may be affected by interference from other threads. For example, if the above-described examples are not synchronized (Synchronization) treatment of the corresponding code, it may occur at the time of performing the second operation, the value of the count variable other thread has been modified. Of course, synchronized keyword can help us achieve atomic operations, in order to avoid interference between this thread.

synchronized keyword atomic operations may be implemented, and its essence is: through the exclusive keyword included in the critical region (Critical Section) to ensure that only one thread can execute a critical section of code at any one time, which makes the critical zone the code representing an atomic operation. This, we basically very clear. However, another role played by the synchronized keyword - to ensure the visibility of memory (Memory Visibility), is also worth recalling our place.

Second, the visibility of memory

  CPU at the time of execution of the code, in order to reduce the time variable access consumption possible value of the variable code accessed cache to the CPU cache, therefore, the corresponding code to access the variable again when the corresponding value may be from the CPU cache instead of main memory read. Similarly, modifications to the code values ​​of these variables are cached only may also be written into the CPU cache, the main memory is not written. Since each CPU has its own buffer, so the contents of a CPU cache in the CPU for other purposes is not visible. This leads to other threads running on other CPU may not be able to "see" the thread changes to a variable value. This is called memory visibility.

Another effect of the synchronized keyword is to ensure that a thread executing a critical section of the code, modify the values ​​of the variables for a thread of execution later this critical area is visible. It is important to ensure the correctness of the multi-threaded code is.

The volatile keyword also ensures memory visibility. That is a thread change the value of a variable using volatile keyword modified, for the other threads access the variable terms are always visible. That is to say, other threads will not read a "date" variable value. Therefore, it was the volatile internal lock and key representatives of the synchronized keyword comparison, referred to as lightweight lock. In fact, this call is not appropriate, volatile keyword can only guarantee the visibility of memory, internal lock it does not like the synchronized keyword to ensure represented as atomic operations. Realize volatile keyword visibility of memory core mechanism is: when a thread changes the value of a volatile variable is modified, the value will be written to main memory (ie RAM) rather than the current CPU thread where the buffer zone , while the value of the variable other CPU cache of stored will therefore fail (in order to update the main memory of the "new value" of the variable). This ensures that when the other threads to access the volatile variable modified, you can always get the latest value of the variable.

Third, instruction reordering

Another effect of the volatile keyword is: it prohibits the reordering instruction (Re-order). Compiler and CPU in order to improve the efficiency of instruction may be an instruction reordering, which makes the actual implementation of the code may not be in accordance with what we think of the way. For example, the following statements initialize instance variables:

private SomeClass someObject = new SomeClass();

The above statement is very simple: (1) create an instance of the class of SomeClass; (2) the referenced class instance to the variable SomeClass someObject. However, due to the effect of reordering the instruction, actual execution of the code sequence may be: (1) the memory space allocated for storing some instances SomeClass; (2) the reference to a variable someObject the memory space; (3) create a class instance of SomeClass. Therefore, when other threads access the value someObject variables, it is only a reference point to get some storage SomeClass instances of memory space only, while the corresponding memory space SomeClass instance initialization can not complete, which could lead to some Italian unexpected results. Prohibiting instruction can be reordered such that the above code is in accordance with our desired sequence (expressed as the code order) is performed.

Although the ban command reordering causes the compiler and CPU instruction may not be for some optimization, but it somehow allow code execution look more in line with our expectations.

Difference Volatile, two synchronized contact

1.volatile essentially telling jvm current variable values in register (working memory) is uncertain, needs to be read from the main memory; synchronized current variable is locked, only the current thread can access the variables, other threads are blocking live.
2.volatile can only be used in a variable level; can be used in the synchronized variables, methods, and the class level.
3.volatile modify variables can only achieve visibility, it does not guarantee atomicity (thread A variable is not modified when the end of another thread B can see the value has been modified, and this variable may be modified, without waiting for the release of A lock because Volatile variable unlocked); synchronized and can ensure visibility and modifications atomic variables.
4.volatile will not cause obstruction thread; synchronized may cause obstruction and thread context switching.
Variable 5.volatile not labeled optimizing compiler; mark may be synchronized variable optimizing compiler.

6. When using the volatile keyword must be cautious, not simply type variables declared volatile, all operations on this variable are atomic operations. When the value of the variable is determined by itself, such as n = n + 1, n ++, etc., volatile keyword will fail. Only when the value of the variables and their operation is independent of the variable when the level of atoms, such as n = m + 1, that is the original level. Therefore, the use volatile key must be careful, if we are not sure, you can use synchronized instead volatile.

7. "locks are expensive", be used with caution lock mechanism.

Fourth, sequential consistency

Order to ensure consistency with the data competition

When the program is not properly synchronized, the data will exist competition. java memory model specification defines competition data as follows:

  • Write a variable in one thread,
  • In another thread reading the same variable,
  • Read and write and are not synchronized to sort through.

When the code contains data competition, execution of the program tend to produce counterintuitive results (an example is the case of the previous chapter). If a multi-threaded program can synchronize correctly, this program will not be a competitive process data.

JMM memory consistency multithreaded programs synchronized correctly made the following guarantees:

  • If the program is properly synchronized, the execution will have the sequential consistency (sequentially consistent) - that is the result of program execution of the program in sequential consistency memory model is the same result (as we shall see immediately, which for programmer is a strong guarantee). Here's synchronization refers to synchronization in a broad sense, including the correct use of the common synchronization primitives (lock, volatile and final) of.

Sequential consistency memory model

 

Sequential consistency memory model is an idealized theoretical computer scientist Reference Model, which provides a strong guarantee for the visibility of memory programmer. Sequential consistency memory model has two main features:

  • All operations must be a thread of execution in program order.
  • (Regardless of whether the program synchronization) that all threads can only see a single sequence of operations performed. In sequential consistency memory model, each operation must be performed and the atom immediately visible to all threads.

Sequential Consistency memory model provided by the programmer to view the following:

 

Conceptually, the sequential consistency model has a single global memory, this memory can be connected to any one of a thread switch by swinging around. At the same time, each thread must be programmed in order to perform memory read / write operations. From the graph we can see that at any point in time at most only one thread can be connected to memory. When a plurality of concurrently executing threads, the switching device of FIG all threads can all memory read / write operations serialized.

To better understand, let's take a further explanation of the characteristics of the sequential consistency model by two diagrams.

Suppose there are two threads A and B are performed concurrently. Wherein A thread has three operations, their order in the program are: A1-> A2-> A3. Thread B has three operations, their order in the program are: B1-> B2-> B3.

Assume that the two threads used to monitor the correct synchronization: A thread releases the monitor after performing three operations, then thread B to acquire the same monitor. The program execution results in the sequential consistency model as shown below:

Now we assume that two threads do not synchronize, the following is a schematic diagram of the implementation of this program is not synchronized sequential consistency model:

No synchronization program in sequential consistency model, although the overall execution order is out of order, but all threads can only see a consistent overall execution order. FIG above example, thread A and B to see the order of execution are: B1-> A1-> A2-> B2-> A3-> B3. We have been able to get this guarantee because sequential consistency memory model each operation must be immediately visible to any thread.

However, the JMM would not have this guarantee. JMM is not a synchronization program, not only in the overall implementation of the order is out of order, but the order thread sees all actions may also be inconsistent. For example, in the current thread the written data cached in local memory, and has not refreshed before the main memory, the write operation is visible only to the current thread; viewed from the perspective of other threads will think that this simply has not write the execution of the current thread. After the refresh only the current thread in the local memory data written to main memory, the write operation to be visible to other threads. In this case, the current thread and the other thread sees the operations performed sequentially inconsistent.

The effect of sequential consistency synchronization program

Here we ReorderExample monitor to synchronize with the previous example program to see how to properly synchronize the program with sequential consistency.

Consider the following sample code:

class SynchronizedExample {
int a = 0;
boolean flag = false;

public synchronized void writer() {
    a = 1;
    flag = true;
}

public synchronized void reader() {
    if (flag) {
        int i = a;
        ……
    }
}
}

上面示例代码中,假设A线程执行writer()方法后,B线程执行reader()方法。这是一个正确同步的多线程程序。根据JMM规范,该程序的执行结果将与该程序在顺序一致性模型中的执行结果相同。下面是该程序在两个内存模型中的执行时序对比图:

在顺序一致性模型中,所有操作完全按程序的顺序串行执行。而在JMM中,临界区内的代码可以重排序(但JMM不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。JMM会在退出监视器和进入监视器这两个关键时间点做一些特别处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图(具体细节后文会说明)。虽然线程A在临界区内做了重排序,但由于监视器的互斥执行的特性,这里的线程B根本无法“观察”到线程A在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。

从这里我们可以看到JMM在具体实现上的基本方针:在不改变(正确同步的)程序执行结果的前提下,尽可能的为编译器和处理器的优化打开方便之门。

未同步程序的执行特性

对于未同步或未正确同步的多线程程序,JMM只提供最小安全性:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false),JMM保证线程读操作读取到的值不会无中生有(out of thin air)的冒出来。为了实现最小安全性,JVM在堆上分配对象时,首先会清零内存空间,然后才会在上面分配对象(JVM内部会同步这两个操作)。因此,在以清零的内存空间(pre-zeroed memory)分配对象时,域的默认初始化已经完成了。

JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为未同步程序在顺序一致性模型中执行时,整体上是无序的,其执行结果无法预知。保证未同步程序在两个模型中的执行结果一致毫无意义。

和顺序一致性模型一样,未同步程序在JMM中的执行时,整体上也是无序的,其执行结果也无法预知。同时,未同步程序在这两个模型中的执行特性有下面几个差异:

  1. 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。这一点前面已经讲过了,这里就不再赘述。
  2. 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。这一点前面也已经讲过,这里就不再赘述。
  3. JMM不保证对64位的long型和double型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。

第3个差异与处理器总线的工作机制密切相关。在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过一系列步骤来完成的,这一系列步骤称之为总线事务(bus transaction)。总线事务包括读事务(read transaction)和写事务(write transaction)。读事务从内存传送数据到处理器,写事务从处理器传送数据到内存,每个事务会读/写内存中一个或多个物理上连续的字。这里的关键是,总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其它所有的处理器和I/O设备执行内存的读/写。下面让我们通过一个示意图来说明总线的工作机制:

如上图所示,假设处理器A,B和C同时向总线发起总线事务,这时总线仲裁(bus arbitration)会对竞争作出裁决,这里我们假设总线在仲裁后判定处理器A在竞争中获胜(总线仲裁会确保所有处理器都能公平的访问内存)。此时处理器A继续它的总线事务,而其它两个处理器则要等待处理器A的总线事务完成后才能开始再次执行内存访问。假设在处理器A执行总线事务期间(不管这个总线事务是读事务还是写事务),处理器D向总线发起了总线事务,此时处理器D的这个请求会被总线禁止。

总线的这些工作机制可以把所有处理器对内存的访问以串行化的方式来执行;在任意时间点,最多只能有一个处理器能访问内存。这个特性确保了单个总线事务之中的内存读/写操作具有原子性。

在一些32位的处理器上,如果要求对64位数据的读/写操作具有原子性,会有比较大的开销。为了照顾这种处理器,java语言规范鼓励但不强求JVM对64位的long型变量和double型变量的读/写具有原子性。当JVM在这种处理器上运行时,会把一个64位long/ double型变量的读/写操作拆分为两个32位的读/写操作来执行。这两个32位的读/写操作可能会被分配到不同的总线事务中执行,此时对这个64位变量的读/写将不具有原子性。

当单个内存操作不具有原子性,将可能会产生意想不到后果。请看下面示意图:

如上图所示,假设处理器A写一个long型变量,同时处理器B要读这个long型变量。处理器A中64位的写操作被拆分为两个32位的写操作,且这两个32位的写操作被分配到不同的写事务中执行。同时处理器B中64位的读操作被拆分为两个32位的读操作,且这两个32位的读操作被分配到同一个的读事务中执行。当处理器A和B按上图的时序来执行时,处理器B将看到仅仅被处理器A“写了一半“的无效值。

 

 

五、final

        根据程序上下文环境,Java关键字final有“这是无法改变的”或者“终态的”含义,它可以修饰非抽象类、非抽象类成员方法和变量。你可能出于两种理解而需要阻止改变:设计或效率。

        final类不能被继承,没有子类,final类中的方法默认是final的。

        final方法不能被子类的方法覆盖,但可以被继承。

        final成员变量表示常量,只能被赋值一次,赋值后值不再改变。

        final不能用于修饰构造方法。

        注意:父类的private成员方法是不能被子类方法覆盖的,因此private类型的方法默认是final类型的。

1、final类

       

1
  final 类不能被继承,因此 final 类的成员方法没有机会被覆盖

,默认都是final的。在设计类时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会载被扩展,那么就设计为final类。

2、final方法

        如果一个类不允许其子类覆盖某个方法,则可以把这个方法声明为final方法。

        使用final方法的原因有二:

        第一、把方法锁定,防止任何继承类修改它的意义和实现。

        第二、高效。编译器在遇到调用final方法时候会转入内嵌机制,大大提高执行效率。

 例如:

public class Test1 { 
public static void main(String[] args) { 
    // TODO 自动生成方法存根 
} 
public void f1() { 
    System.out.println("f1"); 
} 
//无法被子类覆盖的方法 
public final void f2() { 
    System.out.println("f2"); 
} 
public void f3() { 
    System.out.println("f3"); 
} 
private void f4() { 
    System.out.println("f4"); 
} 
} 
public class Test2 extends Test1 { 
     
public void f1(){     
    System.out.println("Test1父类方法f1被覆盖!"); 
} 
public static void main(String[] args) { 
    Test2 t=new Test2(); 
    t.f1();    
    t.f2(); //调用从父类继承过来的final方法 
    t.f3(); //调用从父类继承过来的方法 
    //t.f4(); //调用失败,无法从父类继承获得 
} 
}

3、final变量(常量)

        用final修饰的成员变量表示常量,值一旦给定就无法改变!

        final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。

        从下面的例子中可以看出,一旦给final变量初值后,值就不能再改变了。

        另外,final变量定义的时候,可以先声明,而不给初值,这中变量也称为final空白,无论什么情况,编译器都确保空白final在使用之前必须被初始化。但是,final空白在final关键字final的使用上提供了更大的灵活性,为此,一个类中的final数据成员就可以实现依对象而有所不同,却有保持其恒定不变的特征。

 

package org.leizhimin; 
public class Test3 { 
        private final String S = "final实例变量S"; 
        private final int A = 100; 
        public final int B = 90; 
        public static final int C = 80; 
        private static final int D = 70; 
        public final int E; //final空白,必须在初始化对象的时候赋初值 
        public Test3(int x) { 
                E = x; 
        } 
        /** 
         * @param args 
         */ 
        public static void main(String[] args) { 
                Test3 t = new Test3(2); 
                //t.A=101;    //出错,final变量的值一旦给定就无法改变 
                //t.B=91; //出错,final变量的值一旦给定就无法改变 
                //t.C=81; //出错,final变量的值一旦给定就无法改变 
                //t.D=71; //出错,final变量的值一旦给定就无法改变 
                System.out.println(t.A); 
                System.out.println(t.B); 
                System.out.println(t.C); //不推荐用对象方式访问静态字段 
                System.out.println(t.D); //不推荐用对象方式访问静态字段 
                System.out.println(Test3.C); 
                System.out.println(Test3.D); 
                //System.out.println(Test3.E); //出错,因为E为final空白,依据不同对象值有所不同. 
                System.out.println(t.E); 
                Test3 t1 = new Test3(3); 
                System.out.println(t1.E); //final空白变量E依据对象的不同而不同 
        } 
        private void test() { 
                System.out.println(new Test3(1).A); 
                System.out.println(Test3.C); 
                System.out.println(Test3.D); 
        } 
        public void test2() { 
                final int a;     //final空白,在需要的时候才赋值 
                final int b = 4;    //局部常量--final用于局部变量的情形 
                final int c;    //final空白,一直没有给赋值.    
                a = 3; 
                //a=4;    出错,已经给赋过值了. 
                //b=2; 出错,已经给赋过值了. 
        } 
}
  
4、final参数
        当函数参数为final类型时,你可以读取使用该参数,但是无法改变该参数的值。
  
public class Test4 { 
        public static void main(String[] args) { 
                new Test4().f1(2); 
        } 
        public void f1(final int i) { 
                //i++;    //i是final类型的,值不允许改变的. 
                System.out.print(i); 
        } 
}

二、static

        static表示“全局”或者“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块,但是Java语言中没有全局变量的概念。

 

        被static修饰的成员变量和成员方法独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的所有实例共享。只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。因此,static对象可以在它的任何对象创建之前访问,无需引用任何对象。

 

        用public修饰的static成员变量和成员方法本质是全局变量和全局方法,当声明它类的对象市,不生成static变量的副本,而是类的所有实例共享同一个static变量。

 

        static变量前可以有private修饰,表示这个变量可以在类的静态代码块中,或者类的其他静态成员方法中使用(当然也可以在非静态成员方法中使用--废话),但是不能在其他类中通过类名来直接引用,这一点很重要。实际上你需要搞明白,private是访问权限限定,static表示不要实例化就可以使用,这样就容易理解多了。static前面加上其它访问权限关键字的效果也以此类推。

 

        static修饰的成员变量和成员方法习惯上称为静态变量和静态方法,可以直接通过类名来访问,访问语法为:

类名.静态方法名(参数列表...) 

类名.静态变量名

        用static修饰的代码块表示静态代码块,当Java虚拟机(JVM)加载类时,就会执行该代码块(用处非常大,呵呵)。

 

1、static变量

        按照是否静态的对类成员变量进行分类可分两种:一种是被static修饰的变量,叫静态变量或类变量;另一种是没有被static修饰的变量,叫实例变量。两者的区别是:

        对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。

        对于实例变量,没创建一个实例,就会为实例变量分配一次内存,实例变量可以在内存中有多个拷贝,互不影响(灵活)。

 

2、静态方法

        静态方法可以直接通过类名调用,任何的实例也都可以调用,因此静态方法中不能用this和super关键字,不能直接访问所属类的实例变量和实例方法(就是不带static的成员变量和成员成员方法),只能访问所属类的静态成员变量和成员方法。因为实例成员与特定的对象关联!这个需要去理解,想明白其中的道理,不是记忆!!!

        因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。

 

3、static代码块

        static代码块也叫静态代码块,是在类中独立于类成员的static语句块,可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果static代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。例如:

 

public class Test5 { 
        private static int a; 
        private int b; 
        static { 
                Test5.a = 3; 
                System.out.println(a); 
                Test5 t = new Test5(); 
                t.f(); 
                t.b = 1000; 
                System.out.println(t.b); 
        } 
        static { 
                Test5.a = 4; 
                System.out.println(a); 
        } 
        public static void main(String[] args) { 
                // TODO 自动生成方法存根 
        } 
        static { 
                Test5.a = 5; 
                System.out.println(a); 
        } 
        public void f() { 
                System.out.println("hhahhahah"); 
        } 
}
  
运行结果:
3
hhahhahah
1000
4
5

        利用静态代码块可以对一些static变量进行赋值,最后再看一眼这些例子,都一个static的main方法,这样JVM在运行main方法的时候可以直接调用而不用创建实例。

 

4、static和final一块用表示什么

        static final用来修饰成员变量和成员方法,可简单理解为“全局常量”!

        对于变量,表示一旦给值就不可修改,并且通过类名可以访问。

        对于方法,表示不可覆盖,并且可以通过类名直接访问。

       

        特别要注意一个问题:

        对于被static和final修饰过的实例常量,实例本身不能再改变了,但对于一些容器类型(比如,ArrayList、HashMap)的实例变量,不可以改变容器变量本身,但可以修改容器中存放的对象,这一点在编程中用到很多。

        也许说了这么多,反倒把你搞晕了,还是看个例子吧:

 

public class TestStaticFinal { 
        private static final String strStaticFinalVar = "aaa"; 
        private static String strStaticVar = null; 
        private final String strFinalVar = null; 
        private static final int intStaticFinalVar = 0; 
        private static final Integer integerStaticFinalVar = new Integer(8); 
        private static final ArrayList<String> alStaticFinalVar = new ArrayList<String>(); 
        private void test() { 
                System.out.println("-------------值处理前----------\r\n"); 
                System.out.println("strStaticFinalVar=" + strStaticFinalVar + "\r\n"); 
                System.out.println("strStaticVar=" + strStaticVar + "\r\n"); 
                System.out.println("strFinalVar=" + strFinalVar + "\r\n"); 
                System.out.println("intStaticFinalVar=" + intStaticFinalVar + "\r\n"); 
                System.out.println("integerStaticFinalVar=" + integerStaticFinalVar + "\r\n"); 
                System.out.println("alStaticFinalVar=" + alStaticFinalVar + "\r\n"); 
                //strStaticFinalVar="哈哈哈哈";        //错误,final表示终态,不可以改变变量本身. 
                strStaticVar = "哈哈哈哈";                //正确,static表示类变量,值可以改变. 
                //strFinalVar="呵呵呵呵";                    //错误, final表示终态,在定义的时候就要初值(哪怕给个null),一旦给定后就不可再更改。 
                //intStaticFinalVar=2;                        //错误, final表示终态,在定义的时候就要初值(哪怕给个null),一旦给定后就不可再更改。 
                //integerStaticFinalVar=new Integer(8);            //错误, final表示终态,在定义的时候就要初值(哪怕给个null),一旦给定后就不可再更改。 
                alStaticFinalVar.add("aaa");        //正确,容器变量本身没有变化,但存放内容发生了变化。这个规则是非常常用的,有很多用途。 
                alStaticFinalVar.add("bbb");        //正确,容器变量本身没有变化,但存放内容发生了变化。这个规则是非常常用的,有很多用途。 
                System.out.println("-------------值处理后----------\r\n"); 
                System.out.println("strStaticFinalVar=" + strStaticFinalVar + "\r\n"); 
                System.out.println("strStaticVar=" + strStaticVar + "\r\n"); 
                System.out.println("strFinalVar=" + strFinalVar + "\r\n"); 
                System.out.println("intStaticFinalVar=" + intStaticFinalVar + "\r\n"); 
                System.out.println("integerStaticFinalVar=" + integerStaticFinalVar + "\r\n"); 
                System.out.println("alStaticFinalVar=" + alStaticFinalVar + "\r\n"); 
        } 
        public static void main(String args[]) { 
                new TestStaticFinal().test(); 
        } 
}

 

 

参考文献

  1. JSR-133: Java Memory Model and Thread Specification
  2. Shared memory consistency models: A tutorial
  3. The JSR-133 Cookbook for Compiler Writers
  4. 深入理解计算机系统(原书第2版)
  5. UNIX Systems for Modern Architectures: Symmetric Multiprocessing and Caching for Kernel Programmers
  6. The Java Language Specification, Third Edition
  7. 来源于:https://blog.csdn.net/qq_31997407/article/details/79705767

 

 

 

 

Guess you like

Origin www.cnblogs.com/JonaLin/p/11571244.html