两个关键问题
线程间如何通信及线程之间如何同步?
共享内存并发模型:线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式的通信。
消息传递的并发模型:线程之间没有公共状态,线程必须通过发送消息来显示进行通信
java并发采用的共享内存模型,整个通信过程完全透明。
java内存模型的抽象结构
所有的实例域,静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。局部变量,方法定义参数异常处理参数不会再线程之间共享,他们不会有内存可见性问题,不受内存模型的影响。
java线程通讯java内存模型(JMM)控制。JMM决定一个线程对共享变量的写入何时对另一个线程可见。
内存抽象如下:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。
内存模型整体如下:
从图中可知,如果线程A与线程B之间要通讯的话,必须要经历下面2个步骤
1.线程A把本地内存中更新过得共享变量刷新到主内存中去。
2.线程B到主内存中去读取A之前已经更新过得共享变量
下图是线程之间通信图
本地内存A和本地内存B由主内存中共享变量x的副本.假设初始值,三个内存中的X的值都是0,线程A在执行的时候,把更新后的x的值(假设是1)临时存放在自己本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存修改后的x的值刷新到主内存中,此时主内存中x的值变为了1,随后B线程到主内存中去读取线程A更新后的的X的值,此时线程B本地内存x的值也变为了1。
以上就是线程间通信过程,JMM通过控制主内存与每个线程的本地内存之间的交互,这也就是我们说的内存可见性。保证内存可见性就是通过更改他的这种工作机制让他更新以后彼此都知道。
下面写个例子
代码如下:
这个变量a是被新开的线程A和main线程共享的
package juc.memory;
/**
* @Description
* @Author DJZ-WWS
* @Date 2019/5/11 15:49
*/
public class MyDate {
int a = 0;
public void setA(){
this.a=100;
}
}
测试方法如下:
package juc.memory;
/**
* @Description
* @Author DJZ-WWS
* @Date 2019/5/11 15:48
*/
public class MemoryVisibilityTest {
public static void main(String[] args) {
MyDate myDate=new MyDate();
//开启一个线程更改mydate的值
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myDate.setA();
System.out.println("a的值被更改了,更改为 "+myDate.a);
}
});
thread.setName("线程A");
//开启线程
thread.start();
while (myDate.a==0){
//如果线程跳过这句话,说明值已经被修改了,否则就是上个线程对值的修改main线程并不知道,还保持原来的值0
}
System.out.println("原始数据已经被修改");
}
}
新开了一个线程,在新开的线程里面更改属性的值将值更改为100。注意这样的操作涉及到了上面所说的线程通信
下面有个判断,如果while循环 一直处于循环,说明a的值还是原先的值,并没有发生改变,如果没有一直循环,而是执行打印的那句话这时候说明线程A修改的值main线程知道了。
结果是只打印
a的值被更改了,更改为 100
为什么出现这样的情况呢?
原因很简单,这是上面那个图,我再次摘下来看一下,线程A本地内存修改为100,并且写到了主内存中,但是这样的操作对于main线程来说是不可见的,是无感的main线程并不知道线程A已经对共享数据造成了修改,所以会出现上面那种情况。情况发生了怎么解决呢?
将变量使用valatile修饰一下会发现就好使了,valatile保证了共享变量的内存可见性。关于valatile后面再具体说。同时valatile也可以防止指令重排,也就是下文所说的指令重排。
加上关键字的打印结果如下 解决了内存可见性。
我们常见的还有一个特性,指令重排。指令重排分为三种:
编译器优化重排序
指令级并行的重排序 现在的处理器,如果不存在数据依赖性,处理器可以改变语句对应得机器指令的执行顺序。
也就是对于一些语法上的符合语法要求的顺序,比如定义变量变量定义先后定义的顺序不考虑,但是变量要先定义后使用,这样的顺序必须满足。
内存系统的重排序。处理器使用缓存和读写缓冲区。
从我们的源码到被转换成汇编语言之前,需要经过以上的重排规则。