你真的懂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;
        }
    
        @Override
        public 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;
    }

    @Override
    public 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;
		    }
		
		    @Override
		    public 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;
    }

    @Override
    public 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;
    }

    @Override
    public 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 等著 董云兰等译

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值