Jvm内存模型(更多参阅 JVM内存划分和new时JVM做了什么]

file

:1、用来存储new出来的对象和其属性。2、线程共享

:1、存储运行时的数据(方法、对象、变量等) 2、每个线程都有自己的栈,线程私有  3、先进后出(类似子弹夹最后一个压进去的子弹最先打出来)

方法区:1、字节码文件加载时候存放区域,分为静态区(静态方法、变量、长量)和非静态区2、线程共享

程序计数区:程序执行到某一行的行号指示器,唯一一个不会发生溢出的,也是线程私有

本地native区:提供给java程序调用本地程序的中间程序,线程私有一般用c或者c++写的,我们只管调用即可。

多线程时候经常出现的问题

原子性: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

一个很经典的例子就是银行账户转账问题:比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

file

可见性:是指当多个线程访问同一个共享变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 举个简单的例子:

//线程1执行的代码
int i = 1;
i = 100;
//线程2执行的代码
int j = i;

线程1执行后把i赋值为100了但是线程2不知道,但是线程2中j=i还是1。这是因为线程间没有可见性。

有序性:处理器为了提高程序运行效率,可能会对输入代码进行优化。 如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。 虽然重排序不会影响单个线程内程序执行的结果,但是多线程就会。如下:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2: while(!inited ){ sleep() } doSomethingwithconfig(context);

由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

那么基于Java的内存模型怎样实现以上三大并发特性呢?

对于原子性,在 Java 中,对基本数据类型的变量的读取 和 赋值 操作是原子性操作,即这些操作是不可被中断的: 要么执行,要么不执行。也就是说Java内存模型只保证了基本类型数据的读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过 synchronized 和 Lock 来实现。由于 synchronized 和 Lock 能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

1、volatile关键字

1、保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。 原理:当读取一个volatile变量时,JMM会把该栈的“共享变量的副本”置为无效,那么该线程将只能从主内存中重新读取共享变量。 2、禁止指令重排序优化 先了解一个概念,内存屏障(Memory Barrier),又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化,如下图: file

2、synchronized

synchronized 使得它作用范围内的代码对于不同线程是互斥的,并且线程在释放锁的时候会将共享变量的值刷新到主内存中。

3、volatile和synchronized的比较

根据以上的分析,我们可以发现volatile和synchronized有些相似。 1、当线程对 volatile变量写时,java 会把值刷新到共享内存中;而对于synchronized,指的是当线程释放锁的时候,会将共享变量的值刷新到主内存中。

2、线程读取volatile变量时,会将本地内存中的共享变量置为无效;对于synchronized来说,当线程获取锁时,会将当前线程本地内存中的共享变量置为无效。

3、synchronized 扩大了可见影响的范围,扩大到了synchronized作用的代码块。

关键字volatile 与内存模型紧密相关,是线程同步的轻量级实现,其性能要比 synchronized关键字 好。在作用对象和作用范围上,volatile 用于修饰变量,而 synchronized关键字 用于修饰方法和代码块,而且 synchronized 语义范围不但包括 volatile拥有的可见性,还包括volatile 所不具有的原子性,但不包括 volatile 拥有的有序性,即允许指令重排序。

因此,在多线程环境下,volatile关键字 主要用于及时感知共享变量的修改,并保证其他线程可以及时得到变量的最新值。

4、final变量

final关键字可以修饰变量、方法和类,我们这里只讨论final修饰的变量。final变量的特殊之处在于:

final 变量一经初始化,就不能改变其值。 这里的值对于一个对象或者数组来说指的是这个对象或者数组的引用地址。因此,一个线程定义了一个final变量之后,其他任意线程都可以拿到这个变量。

但有一点需要注意的是,当这个final变量为对象或者数组时: 1、虽然我们不能将这个变量赋值为其他对象或者数组的引用地址,但是我们可以改变对象的域或者数组中的元素。 2、线程对这个对象变量的域或者数据的元素的改变不具有线程可见性