本帖最后由 小蜀哥哥 于 2018-10-30 13:13 编辑
问题一:为什么要学习JMM内存模型
在我们学习Java的第一天就了解到Java是一门通过JVM虚拟机跨平台的语言,同时Java也是一门具有多个线程同时处理能力的编程语言,那么在多个线程并发的情况,我们的Java底层是如何对这些线程进行处理,且又如何保证线程的安全性,这就是学习JMM内存模型的原因以及目的
并发编程分类
并发编程解决的问题是多个线程在怎么样交互数据,简单的说,就是多个线程在处理同一个变量时,如何进行信息的沟通,当下流行的一共有两种通信的机制
1.共享内存
在这种内存模型下,会产生工作内存(将共享数据加载到工作内存中来进行操作,保证数据的高效性)和主内存(存放共享数据),工作内存和主内存之间通过read 和 write 来进行数据的交流
2.消息传递
这种模型是没有共享数据,线程之间必须通过明确的发送消息来显式进行通信。
内存模型的工作方式
Java采用的是共享内存模型
正如上文所介绍的那样,Java会将共享的数据放置到主内存中,当多个线程操作这个共享变量时,会将对应的内容加载到自己的工作内存中来,也就是说,有多少个线程就有多少个工作内存
假设现在有内存A和B有主内存中共享变量x的副本。初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。内存模型中值得注意的三个点
1. 内存可见性
内存可见性的问题指的就是在工作方式中介绍到当A修改了工作内存中的数据,此时B去操作自己工作内存中的数据时, A需要将数据写回到主内存中,B再次从主内存中读取数据,但这有可能情况并不是那么美好,若B没有从主内存中读取数据,那么此时B中的数据和A中的数据此刻将变得没有意义,所以我们将这重在修改了各自工作内存后,没有重新从主内存中读取数据的行为,称之为内存可见性问题,A和B彼此之间数据不可见 [Java] 纯文本查看 复制代码 public class VolatileTest {
public static void main(String[] args) {
MyThread mt = new MyThread();
new Thread(mt).start();
while(true){
if(mt.isFlag()){
System.out.println("------");
break;
}
}
}
}
class MyThread implements Runnable{
private volatile boolean flag = false;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag = "+isFlag());
}
}
在本案例中,我们希望看到的执行结果是,程序会在200毫秒后打印----,同时程序结束,但真正的情况是本程序永远不会结束,原因是while操作是属于底层代码操作,执行效率非常之快,会导致main线程都来不及从主内存中抓取被MyThread修改后的flag,导致main线程一直在读取自己工作内存中的数据,而自己工作内存中的数据一直是false,故而程序不会结束
解决内存可见性问题的手段有很多synchronized、Lock、final、volitile都能够解决内存可见性问题
2.重排序问题
重排序问题,指的是一个线程在不影响编译结果的情况下,会按照JVM喜爱的方式去对程序进行顺序上的调整
用一段伪代码来展示
[AppleScript] 纯文本查看 复制代码 main(){
int i =10; //1
int j =20; //2
int flag = true; //3
int temp = i * j ;//4
}
在这段程序中,我们按照代码的顺序,编号1,2,3,4 我们能够看到只要4在1,2 之后,那么这个程序无论是哪种编译情况都不会影响最终程序的执行结果 ,如: 2,1,4,3 1,3,2,4 ,在单线程情况,这种重排序的方案并没有任何问题,但如果出现了多线程程序 [Java] 纯文本查看 复制代码 class MyExample(){
private boolean flag = false;
int i =5;
int j =5;
read(){
i =10; //1
j = 20; //2
flag = true; //3
}
write(){
while(flag){
int temp = i * j;
}
}
}
我们会发现如果read是一条线程,write是一条线程,那么在单独考虑read线程的情况下, read线程无论是按照1,2,3排序,还是3,2,1或者是任意方式排序,都不会对程序产生任何影响,但此时若考虑到write线程,若read线程时1,2,3 那么write 就是 200,而如果按照3,2,1变成,那么write线程的结果就是25,所以JMM的重排序是一个我们需要考虑的地方,解决的方案是 synchronized、Lock 、volitile 除此之外 Java 内存模型通过 happens-before 原则如果能推导出来两个操作的执行顺序就能先天保证有序性,否则无法保证, happens-before中定义了8种情况,在满足这8种情况,JVM不会对代码进行重排序
具体规则请自行查询
3.原子性问题
[Java] 纯文本查看 复制代码
所谓的原子性操作的含义是指该操作是不可分割的,比如我们通常写的 i = 0; int i = i++;
在底层是由三步组成
int temp = i;
temp = temp + 1;
int i = temp;
使用volitile关键字 是无法保证volitile关键字的,我们可以使用synchronized关键字,以及atomicInteger来解决这样的问题,而atomicInteger底层采用的是cas算法,不属于我们本次的讨论范围
总结
相信同学们在学习完本章内容之后一定对JMM内存模型有了一定的了解,学习是没有止境的,光去看表面是不能解决问题的,在学习到一定阶段之后,这些内容都需要同学们去思考,谢谢大家。
|