成都校区*精品*JMM内存模型

问题一:为什么要学习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] 5

所谓的原子性操作的含义是指该操作是不可分割的,比如我们通常写的 i = 0; int i = i++;

在底层是由三步组成

int temp = i;

temp = temp + 1;

int i = temp;

使用volitile关键字 是无法保证volitile关键字的,我们可以使用synchronized关键字,以及atomicInteger来解决这样的问题,而atomicInteger底层采用的是cas算法,不属于我们本次的讨论范围

总结

相信同学们在学习完本章内容之后一定对JMM内存模型有了一定的了解,学习是没有止境的,光去看表面是不能解决问题的,在学习到一定阶段之后,这些内容都需要同学们去思考,谢谢大家。

成都校区*精品*JMM内存模型


分享到:


相關文章: