[深入理解Java虚拟机]读书笔记(2)Java内存模型

 2019-12-22 11:03  阅读(856)
文章分类:JVM

在现在计算机的硬件构架中,每个处理器都有自己的高速缓冲存储器,用来解决处理器的运算速度和内存之前几个数理级的差距。而各个高速缓存与主内存之间的数据一致性要求则是通过缓存一致性协议来保证的。而Java虚拟机也有自己的内存模型,并且与硬件的缓存操作具有很高的可比性:

2019120001518\_1.png

1.Java内存模型

规定了所有的变量都存储在主内存(Main Memory)中(与物理硬件的命名一致,可以类比,但此处仅是JVM内存的一部分),每个线程还有自己的工作内存(Working Memory,可与高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝(并非完全拷贝,具体的实现有待查证…),线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile同样如此)。

不同线程无法访问其它工作内存中的变量,线程间的变量的交互均需要通过主内存来完成。如上图。

1.1.内存间的交互操作

Java内存模型定义了以下8种操作来完成主内存和工作内存之间的交互:

lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占的状态。

unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定。

read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

load(载入):作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中。

use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。

assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量的赋值的字节码指令时执行这个操作。

store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write操作作使用。

write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。

1.2.对于volatile型变量的特殊规则

volatile有两个语义,其一是保证此变量对所有线程的可见性:对volatile变量的写操作会立刻反应到主内存中,读操作始终会读取主内存中的当前最新值。这里使用当前是因为,Java里的面运算并非原子操作,这会导致volatile变量的运算在并发下是不安全的。下面的代码来自[深入理解JVM虚拟机]的12-1:

/**
     * volatile变量自增运算测试
     * 
     * @author zzm
     */
    public class VolatileTest {
        public static volatile int race = 0;

        public static void increase() {
            race++;
        }

        private static final int THREADS_COUNT = 20;

        public static void main(String[] args) {
            Thread[] threads = new Thread[THREADS_COUNT];
            for (int i = 0; i < THREADS_COUNT; i++) {
                threads[i] = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < 10000; i++) {
                            increase();
                        }
                    }
                });
                threads[i].start();
            }

            // 等待所有累加线程都结束
            while (Thread.activeCount() > 1)
                Thread.yield();

            System.out.println(race);
        }
    }

如果这端代码正确并发的吗,结果应该是200000,但经过多次运行可以看到,输出的结果小于200000。问题出现在自增“race++”上,自增不是原子操作,可以看到class文件中increase()是由此来4条字节码指令构成的(不算return)。从字节码层面上可以分析出并发失败的原因:当getstatic指令把race的值取到操作栈顶时,volatile关键字保证了race的值此时是正确的,但当执行iconst_1、iadd这些指令时,其它线程可能已经把race的值加大了,此时操作栈顶的值就成了过期的数据,所以putstatic指令执行后就有可能把过期的数据写入主面存中。

public static void increase();
        0  getstatic chapter12.VolatileTest.race : int [13]
        3  iconst_1
        4  iadd
        5  putstatic chapter12.VolatileTest.race : int [13]
        8  return
          Line numbers:
            [pc: 0, line: 11]
            [pc: 8, line: 12]

volatile在下面两条规则以外,仍需通过加锁来保证原子性:

①运算结果不依赖于变量的当前值或能够确保只有单一的线程修该变量的值。

②变量不需要与其它的状态变量共同参与不变约束。

其二是禁止指令重排序优化,保证变量赋值操作的顺序与程序代码中的执行顺序一到。具体请参照[单例模式]②双重检查加锁(不推荐)的说明。

1.3.先行发生原则(happens-before)

先行发生原则是Java内存模型中定义的两项操作之间的顺序关系,如果操作A先行发生于操作B,那么操作A产生的影响是能被操作B观察到的,“影响”包括修该了内存中共享变量的值,发送了消息,调用了方法等。它是判断数据是否存在竞争、线程是否安全的主要依据。

下面是Java内存模型下一些”天然的”先行发生关系,无须任何同步已经存在:

程序次序规则(Program Order Rule):在一个线程内,按照程序执行顺序,书写在前面的操作先行发生于书写在后面的操作(控制流顺序而不是程序代码顺序)。

线程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作(同一个锁,时间上的先后)。

volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作(时间上的先后)。

线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。

线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终此检测(可用Thread.isAlive()的返回值等检测线程是否已经终止)。

线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生(Thread.interrupted()方法检测)。

对象终结规则(Finalizer Rule):一个对象的初始化(构造函数执行结束)完成先行发生于它的finalize()方法的开始

传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。

点赞(1)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> [深入理解Java虚拟机]读书笔记(2)Java内存模型

相关推荐