Java是面向对象的语言,每一个Java对象在JVM中存储是有一定的结构的。
Java对象自身的存储模型
JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层展示该Java类。
当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。
下面详细介绍
/** * 描述: 演示重排序的现象“直到达到某个条件才停止”,测试小概率事件 */ public class OutOfOrderExecution { private static int x = 0, y = 0; private static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { Thread one = new Thread(new Runnable() { @Override public void run() { a = 1; x = b; } }); Thread two = new Thread(new Runnable() { @Override public void run() { b = 1; y = a; } }); one.start(); two.start(); one.join(); two.join(); System.out.println("x = " + x + "," + "y = " + y); } }
运行结果:
这4行代码的执行顺序决定了 最终x和y的结果,一共有3种情况:
虽然代码执行顺序可能有多种情况,但是在线程1的内部,也就是:a = 1; x = b;
出现(0, 0)的诡异情况
测试代码:
/** * 描述: 演示重排序的现象 “直到达到某个条件才停止”,测试小概率事件 */ public class OutOfOrderExecution { private static int x = 0, y = 0; private static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { int i = 0; for (; ; ) { i++; x = 0; y = 0; a = 0; b = 0; CountDownLatch latch = new CountDownLatch(3); Thread one = new Thread(new Runnable() { @Override public void run() { try { latch.countDown(); latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } a = 1; x = b; } }); Thread two = new Thread(new Runnable() { @Override public void run() { try { latch.countDown(); latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } b = 1; y = a; } }); two.start(); one.start(); latch.countDown(); one.join(); two.join(); String result = "第" + i + "次(" + x + "," + y + ")"; if (x == 0 && y == 0) { System.out.println(result); break; } else { System.out.println(result); } } } }
会出现x = 0,y =0?那是因为重排序发生了,四行代码的执行顺序的其中一种可能。
在线程1内部的两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令并不是严格的按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序,这里被颠倒的时y = a和b = 1这两行语句。
可以看到指令数变少了,这样执行速度就会变快。
编译器优化:包括JVM,JIT编译器等
CPU指令重排:就算编译器不发生重排,CPU也可能对指令进行重排
内存的“重排序”:线程A的修改线程B却看不到,引出可见性问题(主存和缓存的问题)
/** * 描述: 演示可见性带来的问题 */ public class FieldVisibility { int a = 1; int b = 2; private void change() { a = 3; b = a; } private void print() { System.out.println("b=" + b + ";a=" + a); } public static void main(String[] args) { while (true) { FieldVisibility test = new FieldVisibility(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test.change(); } }).start(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test.print(); } }).start(); } } }
这三种情况可能是多线程执行顺序导致的,
但是,还有一种情况,是内存的可见性问题导致的。
两个线程是不能直接通信的,只有通过访问主存。一个写,一个读。
将上述案例演示代码的a, b的值前面加上volatile,就不会出现可见性的问题了。
Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是,JMM抽象了主内存和本地内存的概念。
这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM的一个抽象,是对于寄存器、一级缓存、二级缓存等的抽象。
JMM有以下规定:
所有的共享变量存在于内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。