你真的懂java内存模型吗?(由一段和你预想相反的代码引发的思考)

编程入门 行业动态 更新时间:2024-10-28 08:20:17

你真的懂java内存模型吗?(由一段<a href=https://www.elefans.com/category/jswz/34/1756171.html style=和你预想相反的代码引发的思考)"/>

你真的懂java内存模型吗?(由一段和你预想相反的代码引发的思考)

1.示例代码

  • 代码
    public class Run {public static void main(String[] args) {ThreadA a = new ThreadA();a.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}a.setRunning(false);a.setCount(100);System.out.println(a.getCount() + "\t" + a.getIsRunning());System.out.println("已经发出停止指令了");}
    }
    
    public class ThreadA extends Thread {private boolean isRunning = true;private int count = 0;public void setRunning(boolean running) {isRunning = running;}public void setCount(int count) {this.count = count;}public int getCount() {return count;}public boolean getIsRunning() {return isRunning;}@Overridepublic void run() {System.out.println("进入了run");setCount(1);while (getCount() == 1 && getIsRunning()) {}System.out.println("线程被停止了");}
    }
  • 结果

    由结果图可得知main中的值已经设置成功,但是程序的停止按钮还是亮得(程序还在运行),由”线程被停止了“也没有输出也可以论证程序确实还在运行。我们明明先让ThreadA线程先启动(main线程sleep了一会),然后设置了isRunning为false,但是ThreadA的while却一直没有结束。这究竟是为什么呢?

2.java内存模型的抽象结构

为什么会出现上述现象呢(若停止了,可添加-sever虚拟机参数后重试),那是 因为线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程用来读/写共享变量的副本。本地内存是JMM(Java 内存模型)的一个抽象概念,并不真实存在。它包括了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。 该模型的抽象示意图如下:

在示例代码中虽然改变了变量的值,但是改变的是本地变量的值也就是main线程的私有拷贝值,而在ThreadA中的变量依旧是刚开始从共享内存中拷贝的副本,值没有发生改变,因此程序一直运行而没有停止。
那么从上图观察,如果两个线程之间要使用共享变量进行通信,那么必须经过主内存:
1. 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2. 线程B到主内存中去读线程A之前已更新过的共享变量

由此可得出线程间使用共享变量通信抽象如下:

如图2所示,本地内存A和B中都有共享变量x得副本。假设初始为0,则初始状态下三个内存中得值都为0,线程A在执行时,令自己得本地内存变量x=1,然后将改变后的x刷新到主内存,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B得本地内存的x值也变为1了。从整体来看,这两个步骤实际上是A向B线程发送消息。

3.Happens-Before简介

从JDK5开始,Java使用新的JSR-133内存模型。它使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,如a=1;b=2;可能经过重排序之后先执行b=2;

4.代码的改正

1.使用synchronized

public class Thread extends Thread {private boolean isRunning = true;private int count = 0;public void setRunning(boolean running) {isRunning = running;}public void setCount(int count) {this.count = count;}public synchronized int getCount() {return count;}public synchronized boolean getIsRunning() {return isRunning;}@Overridepublic void run() {System.out.println("进入了run");setCount(1);while (getCount() == 1 && getIsRunning()) {}System.out.println("线程被停止了");}
}

main不变。
说明:

  1. synchronized的内存语义:
    1)获取锁时,使本地内存变量失效,在使用共享变量时需要从主内存中加载。
    2)锁释放时,将本地内存中的共享变量同步到主内存当中。
  2. 程序解释
    在ThreadA中while中调用了同步方法,保证了本地内存中的共享变量的可见性。因此在main中修改变量后对于ThreadA可见,所以跳出了循环。

2.synchronied代码块

	public class ThreadA extends Thread {private boolean isRunning = true;private int count = 0;public void setRunning(boolean running) {isRunning = running;}public void setCount(int count) {this.count = count;}public int getCount() {return count;}public boolean getIsRunning() {return isRunning;}@Overridepublic void run() {System.out.println("进入了run");setCount(1);while (getCount() == 1 && getIsRunning()) {synchronized("anything"){}}System.out.println("线程被停止了");}}

3. volatile

public class ThreadA extends Thread {private volatile boolean isRunning = true;private volatile int count = 0;public void setRunning(boolean running) {isRunning = running;}public void setCount(int count) {this.count = count;}public int getCount() {return count;}public boolean getIsRunning() {return isRunning;}@Overridepublic void run() {System.out.println("进入了run");while (getCount() == 1 && getIsRunning()) {}System.out.println("线程被停止了");}
}

说明:
volatile写具有和锁释放有相同的语义,读和锁的获取具有相同的语义。但是不具有互斥性,只具备了synchronized的可见性

4一些其他比较少见的方案。

  1. 在while中加入System.out.println(“1”);,实际上这个和加入同步代码块是等效的。又比如在while中加入new ThreadA(),也可以停止,因为在类加载初始化时会有锁的获取和释放的动作。
    public void println(String x) {synchronized (this) {print(x);newLine();}}
  1. 在while中加入volatile读或者volatile写都可实现可见性。
public class Run {public static void main(String[] args) {ThreadC c = new ThreadC();c.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}c.setRunning(false);System.out.println(c.getCount() + "\t" + c.getIsRunning());System.out.println("已经发出停止指令了");}
}public class ThreadA extends Thread {private boolean isRunning = true;private volatile int count = 0;public void setRunning(boolean running) {isRunning = running;}public void setCount(int count) {this.count = count;}public int getCount() {return count;}public boolean getIsRunning() {return isRunning;}@Overridepublic void run() {System.out.println("进入了run");while (getIsRunning()) {getCount();}System.out.println("线程被停止了");}
}

说明:在while中仅对count这个volatile变量读取(换成setCount(1),同样成立),而在main中设置isRuning值,但代码却停止了。究其原因是JMM使用内存屏障实现了volatile和synchronized的语义,而内存屏障会触发CPU缓存一致性协议MESI中存储的命令执行。
由于篇幅原因,关于内存屏障和EMSI有兴趣的可以查看上文字中的链接。

参考书籍:
深入理解Java虚拟机 -周志明著
Java 多线程编程核心技术 -高岩洪著
Java并发编程的艺术 方腾飞 魏鹏 程晓明 著
Java Concurrency in Practice Joshua Bloch 等著 董云兰等译

更多推荐

你真的懂java内存模型吗?(由一段和你预想相反的代码引发的思考)

本文发布于:2024-03-06 18:23:52,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1716042.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:和你   你真   模型   内存   代码

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!