关于JMM内存模型的一些个人理解

JMM定义的八个内存间交互操作(原子操作)

  1. lock: 作用于主内存的变量,把一个变量标记为线程独占。
  2. unlock: 作用于主内存的变量,释放被lock操作标记为独占状态的变量。
  3. read: 作用于主内存的变量,将其读入到工作内存中。
  4. load: 作用于工作内存的变量,把read读到的变量存入相应的变量副本中。
  5. use: 作用于工作内存的变量,将变量副本的值传递给执行引擎。当虚拟机遇到一个需要变量值的字节码指令执行这个操作。
  6. assign: 作用于工作内存的变量,将执行引擎的值存入变量副本中。当虚拟机遇到变量赋值时执行这个操作。
  7. store: 作用于工作内存的变量,将工作内存的变量值存入主内存中。
  8. write: 作用于主内存的变量,将store传递的值放入主内存中对应的变量中。

JMM八个操作需要满足的八个规则:

  1. 不允许read和load,store和write操作单独出现。
  2. 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中修改了以后一定要同步回主内存中。
  3. 不允许一个线程无原因的(即没有发生assign)把数据同步回主内存中 。
  4. 一个新的变量只能在主内存中诞生。
  5. 一个变量在同一时间只允许一条线程对其lock操作,同一线程可以多次lock同一个对象。
  6. 一个线程在未执行lock操作之前不允许执行unlock操作。A线程lock的对象只能由A线程unlock。
  7. 如果对一个变量进行lock操作那么将会清空工作内存中此变量的值,在执行引擎用这个变量之前需要重新执行load或assign操作。即重新从主内存中读取这个变量。
  8. 对一个变量进行unlock操作之前,必须先把此变量同步回主内存中。

看完了以上JMM内存模型的上述规范后,我们写几个Demo代码实测一下.

code1: 使用volatile修饰共享变量isStop

public class JMMTest {
    private volatile static boolean isStop = false;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread() {
            public void run() {
            	System.out.println(this.getName() + ": enter");
                 while (!isStop) {
                     ;
                }
                System.out.println(this.getName() + ": exit");
            }
        };
        thread.start();
		// 让主线程休眠一秒钟
        TimeUnit.SECONDS.sleep(1);
        isStop = true;
        System.out.println(Thread.currentThread().getName()+"-isStop : " + isStop);
    }
}

代码运行结果如下:

Thread-0: enter
Thread-0: exit
main-isStop : true

结果分析:熟悉volatile的话这个结果是显而易见的,volatile三大语义:可见性、非原子性和禁指令重排。code1中我们主要用到的就是可见性,即临界区内的共享变量在任何一个线程中的修改对于其他线程都是立即可见的。当我们在主线程设置isStop = true,thread-0线程对于这一修改时立即可见的,此时while(!isStop)循环条件为假,退出循环。thread-0线程执行结束。

注:为什么我们要让主线程休眠一秒钟?主要原因是这样我们可以保证thread-0线程可以在主线程的isStop = true执行之前执行。否则thread-0一开始获取到的就是true值了。

code2:不使用volatile修饰isStop

public class JMMTest {
    private static boolean isStop = false;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread() {
            public void run() {
            	System.out.println(this.getName() + ": enter");
                 while (!isStop) {
                     ;
                }
                System.out.println(this.getName() + ": exit");
            }
        };
        thread.start();
		// 让主线程休眠一秒钟
        TimeUnit.SECONDS.sleep(1);
        isStop = true;
        System.out.println(Thread.currentThread().getName()+"-isStop : " + isStop);
    }
}

运行结果如下:
在这里插入图片描述
结果分析:此时代码不能正常结束,thread-0线程无法退出。也就是说thread-0线程的工作内存中拷贝的变量isStop的值一直为false。原因无非有二:①main线程修改后并未将值刷回主内存中。参见JMM内存交换的第三条规则,我们知道这个假设不成立,即main线程一定会将isStop = true刷回主存。②main线程已经将isStop = true刷回主存,但thread-0线程一直未去主存中获取最新值。显然②才是说得通的。我们知道volatile是保证可见性的,每次线程使用被volatile修饰的变量都会自动的去主存中获取最新值。那么难道没有volatile修饰时,当前的工作线程就永远不去主存获取新值了吗? 事实上即使没有volatile修饰当前的工作线程也是会去主存中获取新值的,只是Java虚拟机规范确实并没有明确的规定无volatile修饰工作线程时何时去主存中同步新值。那么既然会去主存中获取新值为何代码却没有正常的退出呢?其实是JIT优化的锅。参见知乎大神RednaxelaFX关于问题 下面的代码 Java 线程结束原因是什么?的回答。真实原因就是Hotspot Server Compiler(即C2编译器)对代码做的一个激进优化Expression Hoisting. 此处isStop是一个静态变量,本来编译器应该对其保守处理的,但是编译器发现循环题是一个类似于叶子方法的代码块(即其中没有调用任何的其他方法)—只要代码块在执行,在同一线程内不可能有其他代码观测到isStop值的变化。所以此时编译器将会冒进优化,将isStop当作循环不变量(因为代码块内部没有对其值做修改),将其读取操作提升(hoist)到循环之外。被优化的循环代码块就变成了如下的样子:

boolean hoistIsStop = isStop;
while(!hoistIsStop){
	;
}

当代码被优化成如上形式以后,这个程序自然就无法正常执行结束了。既然是由于JIT优化的原因导致的问题,那么就是说如果我们禁用掉JVM的JIT编译代码就能正常结束了,真的是这样吗?我们可以来做一个测试:实验中我们添加一条虚拟机参数:-Xint。限定JVM解释执行此代码从而禁用JIT。代码正常结束,运行结果如下:

Thread-0: enter
Thread-0: exit
main-isStop : true

code3: 在code2代码的while(!isStop)中加入一行输出语句

public class JMMTest {
    private static boolean isStop = false;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread() {
            public void run() {
            	System.out.println(this.getName() + ": enter");
                 while (!isStop) {
                     System.out.println("running");
                }
                System.out.println(this.getName() + ": exit");
            }
        };
        thread.start();
		// 让主线程休眠一秒钟
        TimeUnit.SECONDS.sleep(1);
        isStop = true;
        System.out.println(Thread.currentThread().getName()+"-isStop : " + isStop);
    }
}

运行结果:

Thread-0: enter
running
...    // 省略n条running语句
Thread-0: exit
main-isStop : true

结果分析:此时isStop并没有被volatile修饰,为什么程序可以正常结束呢?code3只是在code2的while(!isStop)的空循环体中加入了一句打印语句,问题一定在出在println()中。进入println()方法查看它的源代码如下:

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

synchronized代码块,似乎很可疑。我们再来仔细看看JMM内存操作的最后两个规则:

  • 如果对一个变量进行lock操作那么将会清空工作内存中此变量的值,在执行引擎用这个变量之前需要重新执行load或assign操作。即重新从主内存中读取这个变量。
  • 对一个变量进行unlock操作之前,必须先把此变量同步回主内存中。
    这两个规则说明了一个问题:就是说当线程每次进入同步代码块或同步方法时,会对主内存中的对象进行lock操作,这个操作会导致工作内存中的拷贝变量被清空,这样线程就必须重新去获取值。这不就是说 **synchronized也保证可见性! ** 那么lock操作到底是lock的哪些对象呢?synchronized(Object){}中的锁对象Object吗?
    显然不是或者说不仅仅只是,因为println()方法的锁对象this指的是System.out对象。而从code3的结果我们可以看出来isStop这个对象也被lock了。 **疑问:lock操作到底会lock哪些对象呢? **

以上被画删除线的内容是我一开始想当然的错误理解,最初我想当然的认为代码正常结束是因为println是同步方法。引用RednaxelaFX的原话来解释这一现象:

这个println()调用在HotSpot Server Compiler 的实现里无法完全内联到底,总是得留一个未内联的方法调用。未内联的方法调用,从编译器的角度来讲是一个“full memory kill”,也就是说副作用不明、必须对内存的读写操作保守处理。保守处理的具体实现就是,就算上一轮我已经读取过了isStop的值了,经过了一段副作用不明的地方,下一次访问时就必须重新读取了。

事实上经过我的测试,在循环体内部调用Arrays.sort(new int[]{})方法代码也能正常退出。

code4: 将code2中的run方法改为synchronized方法


class Flag{
	boolean flag = false;
}
public class JMMTest {
    private static Flag isStop = new Flag();
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread() {
            public void run() {
            	synchronized(Flag){
					System.out.println(this.getName() + ": enter");
                 	while (!isStop) {
                     	;
                	}
                	System.out.println(this.getName() + ": exit");
				}           	
            }
        };
        thread.start();
		// 让主线程休眠一秒钟
        TimeUnit.SECONDS.sleep(1);
        isStop.flag = true;
        System.out.println(Thread.currentThread().getName()+"-isStop : " + isStop.flag);
    }
}

运行结果:
在这里插入图片描述
结果分析:代码无法结束,这是怎么回事,不是说好的synchronized保证可见性吗?其实深入理解一下JMM规则后两条我们不难解释这一现象出现的原因:lock操作时才去获取值,即thread-0线程进入同步方法时获取isStop,此时值为false。而后main线程醒来将isStop改为true并刷会主内存,但是thread-0线程一直在循环体中,并没有再次触发内存交换。从中我们发现,synchronized的可见性和volatile的可见性还是有区别的?

code5:将code4中的同步方法改为同步代码块,并将synchronized代码块置于循环体内

public class JMMTest {
    private static boolean isStop = false;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread() {
            public void run() {
            	System.out.println(this.getName() + ": enter");
                 while (!isStop) {
                     synchronized (this){}
                }
                System.out.println(this.getName() + ": exit");
            }
        };
        thread.start();
		// 让主线程休眠一秒钟
        TimeUnit.SECONDS.sleep(1);
        isStop = true;
        System.out.println(Thread.currentThread().getName()+"-isStop : " + isStop);
    }
}

运行结果:

Thread-0: enter
Thread-0: exit
main-isStop : true

结果分析:代码正常退出的了。有两种可能原因: 1.lock 和 unlock操作不仅仅只是针对synchronized的锁对象,因为此处锁对象是this,如果仅仅指的是lock和unlock锁对象不应该会触发isStop的内存交换。2. synchronized(lockObject){}语句禁用了JIT优化。我个人现在更偏向第二种解释,不过暂时还没有办法排除第一种可能,具体unlock和lock是不是操作的就是锁对象呢?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值