您的位置:首页 > 编程语言 > Java开发

java基础知识(五)java内存模型和volatile关键字

2017-11-17 18:53 621 查看
关于这方面的知识跟线程进程方面的知识关系较大,所以在接触这方面的之前个人感觉默认是已经了解掌握进程线程方面的知识了。

关于这方面的内容,个人感觉http://www.cnblogs.com/dolphin0520/p/3920373.html这位老哥的总结已经是非常的好了,可以去看看这位老哥的总结。这里我只是稍微自己做一下学习笔记。

java内存模型

Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model ,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

java中所有的变量都存储在主内存中(这里的主内存指的应该是cup能够访问到的内存),然后每个线程都拥有着自己的工作内存空间(这里指的的应该是寄存器或者高速缓存)在虚拟机运行的时候,每次把主内存中的变量拷贝一份到自己的工作内存中,在操作完成之后再把该变量重新返回到主内存中。大概就长下面图片中的这样。



在java的内存模型中定义了一共8种操作:

lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。

assign (赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

write(写入):作用于主内存的变量,它把st ore操作从工作内存中得到的变量的值放入主内存的变量中。

在将变量进行操作的时候,变量之后经过read和load操作之后才能进行use或者assign,而且对于同一个变量,read和load操作必须是有先后顺序的,但是可以不要求连续执行。同理store和write操作也是是有先后顺序的,但是可以不要求连续执行。

同时,内存模型还规定了在执行的时候必须遵循下面8个规则:

不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。

不允许一个线程丢弃它的最近的assign 操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

不允许一个线程无原因地(没有发生过任何assign 操作)把数据从线程的工作内存同步回主内存中。

一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assig n 和load操作。

一个变量在同一个时刻只允许一条线程对其进行lock 操作,但lock 操作可以被同一条线程重复执行多次,多次执行lock 后,只有执行相同次数的unlock 操作,变量才会被解锁。

如果对一个变量执行lock 操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign 操作初始化变量的值。

如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock 操作,也不允许去unlock 一个被其他线程锁定住的变量。

对一个变量执行unlock 操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

但是对于long和double类型的数据,内存模型允许在操作的时候将64位长度的变量分为2个32位进行操作,所以可能在操作的时候有可能只能操作到变量的高32位或者低32位。但是虽然这样操作是允许的,但是在虚拟机的是实现的时候,都是把64位数据类型 当做原子操作进行。

原子性、可见性、有序性

原子性(Atomicity)

原子性这个概念相信在很多地方都有听说,即是说一个或者多个操作要么都做要么都不做。在数据库中的事务也具有这个特点。

内存模型中,原子性的操作包括read,load,assign,use,store,write这6个操作,同时在同步代码块中的操作(用lock/unlock和synchronize括起来的操作)也是被认为是具有原子性。

可见性(visibility)

在java内存模型中,可见性指的就是线程修改了一个共享变量的值之后能够立即被其他的线程知道,这个特点在volatile关键字有很好的体现,待会讲到。

在java中可见性除了volatile之外还有synchronize和final也是具有可见性的。

有序性(ordering)

在本线程中观察,所有的操作的都是有序的,在另一个线程中观察,所有的操作都是无序的。

在有序性这里需要提到一个概念,叫做指令重排,指的是CPU采用了允许将很多条指令不按程序的顺序分开发送给相应电路单元处理。简单来讲,就是CPU为了自己的处理方便,可能将程序中的运行顺序重新排序,但是可以保证程序的结果不会有变化。例如下面的代码,CPU就有可能先执行
int j = 1;
这个语句,因为下面的这两个语句无论谁先执行,起码在当前线程中是不会对结果产生如何影响的。但是,可能对于其他线程而言,影响可能就会很大。

int i = 0;
int j = 1;


volatile关键字

可见性

volatile关键字最重要的功能就是实现了可见性,在A线程对X变量进行修改的时候,B线程可以立刻知道X当前的值,保证了不会读到脏数据。

假定T表示一个线程,V 和W分别表示两个volatile型变量,那么在进行read、load、use、assign、store和write操作时需要满足如下规则:

只有当线程T对变量V 执行的前一个动作是load的时候,线程T才能对变量V 执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V 执行load动作。线程T对变量V 的use动作可以认为是和线程T对变量V 的load、read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必
4000
须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)。

只有当线程T对变量V 执行的前一个动作是assign的时候,线程T才能对变量V 执行store动作;并且,只有当线程T对变量V 执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程T对变量V的assign动作可以认为是和线程T对变量V 的store、write动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V 后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V 所做的修改)。

有序性

同时volatile禁止了指令重排序,因为在多线程中,指令重排序对本线程可能不会有印象,但是对另一个线程的影响可能就会很大,这里就不举例子了。

原子性

volatile关键字是无法保证原子性的,就是用volatile修饰的变量的操作一般不能保证是原子操作。

看一个很经典的例子,有一点java基础的应该都看得懂。这个例子一共启用了20个线程,每个线程对变量自增1000次,按照常理,最后输出的应该是20000,但是不管运行多少次,这里输出的值都小于20000。

public class Test {

public static volatile int race = 0;

public static void increase() {
race++;
}

public static void main(String[] args) {
Thread[] threads = new Thread[20];
for (int i = 0; i < 20; i++) {
threads[i] = new Thread(() -> {
for (int i1 = 0; i1 < 1000; i1++) {
increase();
}
});
threads[i].start();
}
//等待所有累加线程都结束
while (Thread.activeCount() > 1)
Thread.yield();
System.out.println(race);
}
}


这是因为在
race++
语句中,这个指令其实并不是原子操作,当get static指令把race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但是在执行iconst _1、iadd这些指令的时候,其他线程可能已经把race的值加大了,而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步回主内存之中。

所以为了保证原子性,volatile关键字一般和synchronize关键字一起使用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 内存 虚拟机 线程