JAVA内存模型,java模型
JAVA内存模型,java模型
Java内存模型规范了Java虚拟机与计算机内存是如何协同工作的。Java虚拟机是一个完整的计算机模型,因此这个模型自然也包含内存模型–又称为Java内存模型
Java内存模型内部原理
Java内存模型把Java虚拟机划分为线程栈和堆。
每一个运行在Java虚拟机中的线程都有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点的相关信息。一个线程仅能够访问自己的线程栈。一个线程创建的本地变量对其他线程不可见,即使两个线程执行同样的代码,这两个线程依然在自己的线程栈中的代码来创建本地变量,因此每个线程拥有本地变量的独有版本。
所有的原始变量都存放在线程栈上,因此对其他线程不可见。
堆上包含在Java程序中创建的所有对象,无论是哪一个对象创建的。这包括原始类型的对象版本。如果一个对象创建后被赋值给另一个局部变量,这个对象依旧在堆上。
一个本地变量可能是原始类型,在这种情况下,它总是待在线程栈上。
一个本地变量可能是指向一个对象的引用,这种情况下对象存放在堆上,本地变量存放在线程栈上面。
一个对象可能包含方法,这些方法可能包含本地变量,本地变量存放在堆上。
一个对象的成员的成员变量可能随着这个对象存放在堆上。不管这个成员变量是原始类型还是引用类型。
静态成员变量随着类的定义,一起定义在堆上。
存放在堆上的对象可以被所持有这个对象的引用访问。当一个线程可以访问一个对象的时候,也可以访问这个对象的成员变量。如果两个线程同时调用一个对象的一个方法,他们都会访问这个对象的成员变量,但是都拥有这个变量的私有拷贝。
两个线程拥有一系列的本地变量。其中一个本地变量执行堆上的一个共享对象。这两个线程分别拥有同一个对象的不同引用,这些引用都是本地变量,指向堆上的同一个对象。
注意,这个共享对象(Object 3)持有Object2和Object4一个引用作为其成员变量(如图中Object3指向Object2和Object4的箭头)。通过在Object3中这些成员变量引用,这两个线程就可以访问Object2和Object4。
这张图也展示了指向堆上两个不同对象的一个本地变量。在这种情况下,指向两个不同对象的引用不是同一个对象。理论上,两个线程都可以访问Object1和Object5,如果两个线程都拥有两个对象的引用。但是在上图中,每一个线程仅有一个引用指向两个对象其中之一。
public class MyRunnable implements Runnable(){
public void run(){
methodOne();
}
public void methodOne() {
int localVariable1 = 45;
MySharedObject localVariable2 =
MySharedObject.sharedInstance;
//... do more with local variables.
methodTwo();
}
public void methodTwo() {
Integer localVariable1 = new Integer(99);
//... do more with local variable.
}
}
public class MySharedObject {
//static variable pointing to instance of MySharedObject
public static final MySharedObject sharedInstance =
new MySharedObject();
//member variables pointing to two objects on the heap
public Integer object2 = new Integer(22);
public Integer object4 = new Integer(44);
public long member1 = 12345;
public long member1 = 67890;
}
如果两个线程同时调用run()方法,就会出现上述情形。
run()方法调用methodOne()方法,methodOne()方法调用methodTwo()方法。
methodOne()方法声明了一个一个原始类型的本地变量和一个引用类型的本地变量。
每个线程执行methodOne()都会在它们对应的线程栈上创建localVariable1和localVariable2的私有拷贝。localVariable1变量彼此完全独立,仅“生活”在每个线程的线程栈上。一个线程看不到另一个线程对它的localVariable1私有拷贝做出的修改。
每个线程执行methodOne()时也将会创建它们各自的localVariable2拷贝。然而,两个localVariable2的不同拷贝都指向堆上的同一个对象。代码中通过一个静态变量设置localVariable2指向一个对象引用。仅存在一个静态变量的一份拷贝,这份拷贝存放在堆上。因此,localVariable2的两份拷贝都指向由MySharedObject指向的静态变量的同一个实例。MySharedObject实例也存放在堆上。它对应于上图中的Object3。
注意,MySharedObject类也包含两个成员变量。这些成员变量随着这个对象存放在堆上。这两个成员变量指向另外两个Integer对象。这些Integer对象对应于上图中的Object2和Object4.
注意,methodTwo()创建一个名为localVariable的本地变量。这个成员变量是一个指向一个Integer对象的对象引用。这个方法设置localVariable1引用指向一个新的Integer实例。在执行methodTwo方法时,localVariable1引用将会在每个线程中存放一份拷贝。这两个Integer对象实例化将会被存储堆上,但是每次执行这个方法时,这个方法都会创建一个新的Integer对象,两个线程执行这个方法将会创建两个不同的Integer实例。methodTwo方法创建的Integer对象对应于上图中的Object1和Object5。
还有一点,MySharedObject类中的两个long类型的成员变量是原始类型的。因为,这些变量是成员变量,所以它们任然随着该对象存放在堆上,仅有本地变量存放在线程栈上。
硬件内存架构
一个现代计算机通常有两个或者多个CPU,其中一些CPU还有多核。从这一点可以看出,在一个有两个或者多个CPU的现代计算机同时运行多个线程是可能的。
每个CPU都包含一系列的寄存器,它们是CPU内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存。
CPU还有一些缓存层
通常情况下,当一个CPU需要读取主存时候,它会将主存的部分读到CPU缓存中。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新的缓存中,然后在某个时间点将值刷新回主存。
当CPU需要在缓存层存放一些东西的时候,存放在缓存中的内容通常被刷新回主存。CPU缓存可以在某一个时刻将数据局部写到它的内存中,和在某一个时刻刷新它的内存。它不会再某一个时刻读写整个缓存。通常,在一个被称为”cache lines”的更小的内存块被更新。
Java内存模型和硬件架构之间的连接
Java内存模型和硬件架构之间的区别就是,硬件架构之间没有区分栈和堆。对于硬件,所有的线程栈和堆都分布在主存中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU的内部寄存器中。
当对象和变量被存放在计算机中各种不同的内存区域的时候,就会出现一些问题,如下:
共享对象的可见性
如果两个或者更多的线程在没有正确的使用volatile声明或者同步的情况下共享一个对象,这个线程更新一个对象,其余线程是不可见的。
想象一下,共享对象被初始化在主存中。跑在CPU的一个线程将这个共享对象读取到CPU缓存中,然后修改了这个对象。只要CPU的缓存么有被刷新会主存,对象修改后的版本对其他线程是不可见的。这个方式可能导致每个线程拥有这个对象的私有拷贝,每个拷贝停留在不同的CPU得缓存中。
上述情况可以如下:
解决办法是使用Volatile关键字,volatile关键字可以保证直接从读取一个变量,如果这个变量被修改后,总是会被写回到主存中。
Race Conditions
如果两个或者更多的线程共享一个对象,多个线程在这个共享对象上更新变量,就有可能发生race conditions。
想象一下,如果线程A读一个共享对象的变量count到它的CPU缓存中。再想象一下,线程B也做了同样的事情,但是往一个不同的CPU缓存中。现在线程A将count加1,线程B也做了同样的事情。现在count已经被增在了两个,每个CPU缓存中一次。
如果这些增加操作被顺序的执行,变量count应该被增加两次,然后原值+2被写回到主存中去。
然而,两次增加都是在没有适当的同步下并发执行的。无论是线程A还是线程B将count修改后的版本写回到主存中取,修改后的值仅会被原值大1,尽管增加了两次
解决这个问题可以使用Java同步块。一个同步快可以保证在同一个时刻仅有一个线程可以进入代码的临界区。同步代码块还可以保证代码块所有被访问的变量将从主从中读入,当线程退出同步代码块时候,所有被更新的变量都会被刷新回主存中,不管这个变量是否被声明为volatile.
相关文章
- 暂无相关文章
用户点评