jvm内存模型

一、Java内存模型定义

        Java内存模型规定了所有的变量都存储在主内存,每条线程还有自己的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,三者关系的交互如图所示:

jvm内存模型_第1张图片
java memory model

      1.1、单线程、本地内存、主内存是如何进行变量的数据交互?

                内存模型JMM定义了8中操作来完成三者之间的数据交互,虚拟机保证每一种操作都是原子性的。

            Lock:作用于主内存的变量,它把一个变量标识为线程独占状态

            Unlock:作用于主内存的变量,将一个处于锁定状态下的变量释放出来,释放后的变量才可以被其他线程加锁。

            Read:作用于主内存的变量,将一个变量的值从主内存传输到线程的工作内存。

            Load:作用于工作内存的变量,将read操作得到的变量的值放入工作内存中的变量副本中

            Use:作用于工作内存的变量,将工作内存中变量的值传递给执行引擎,当虚拟机需要使用时会执行这个操作。

            Assign:作用于工作内存的变量,将一个执行引擎接收到的值赋给工作内存中的变量,当虚拟机遇到给变量赋值的字节码时会执行此操作。

            Store:将工作内存中的一个变量值传送到主内存中

            Write:作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

      1.2、线程A与线程B之间如何进行变量的数据交互?

                1.2.1、 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。

                1.2.2、 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。


二、不得不提的基础概念:

        2.1、内存屏障

            内存屏障是一个CPU指令,它的作用是:1、当CPU处理指令时,插入一条Memory Barrier会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序;2、Memory Barrier所做的另外一件事是强制刷出各种CPU cache,确保变量的内存可见性。

            例子如:Write-Barrier将刷出在Barrier之前写入到本地内存的数据,更新数据到主存中;Read-Barrier将主存中最新数据刷出到每个线程的本地内存中

      2.2、指令重排序

            指令排序是:编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。

            指令重排序的目的是提高程序的运行并发度,发生在编译器、runtime和处理器阶段,遵循as-if-serial语义(不管怎么重排序,单线程程序的执行结果不能改变),也就是重排序所带来的问题是针对多线程的。

            重排序发生的条件是A和B没有存在依赖关系,这里的依赖关系是指“数据依赖关系”和“控制依赖关系”两种。其中数据依赖表示两个以上操作访问同一个变量,且这两个操作中有一个为写操作。而控制依赖关系,比如if(a>0){int i = a*a;}。

        2.3、变量原子性

          访问基本类型的字段的值,或是对其更新操作的时候,除开long类型和double类型,其他类型字段的程序中操作,都将具有原子性操作。例子如:int num=0; 原子性,int numA = numB,[同时存在访问和更新操作],非原子性。

      2.4、变量可见性

            当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。


三、根据以上几个基础概念,解析并发编程中的几个关键字

    3.1、volatile(禁止重排序、变量可见性)

            使用volatile关键字时,在读取变量时,会在读取之前多出一个Read-Barrier指令;在写入变量时,会在写入之后多出一个Write-Barrier指令,保证了变量的可见。它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面。

            例子如:

jvm内存模型_第2张图片
volatile使用

        可能有人这段程序的输出结果小于10000,感到惊讶。但事实的确会如此。

        volatile关键字能保证可见性,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

  自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

  假如某个时刻变量inc的值为10,

  线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

  然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。

  然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

  那么两个线程分别进行了一次自增操作后,inc只增加了1。

    3.2、synchronized(禁止重排序、变量可见性、变量原子性)

        synchronized只是一个内置锁的加锁机制,当某个方法或代码块加上synchronized关键字后,就表明要获得该内置锁才能执行,并不能阻止其他线程访问不需要获得该内置锁的方法。

        java的内置锁:每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。

        java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,知道线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。

        java的对象锁和类锁:java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。更深入理解synchronized关键字传送门!

    3.3、ThreadLocal

        ThreadLocal并非一个线程,而是一个线程局部变量。它的作用就是为使用该变量的线程都提供一个变量值的副本,每个线程都可以独立的改变自己的副本,而不会和其他线程的副本造成冲突。

        ThreadLocal是解决线程安全问题一个很好的思路,ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本,由于Key值不可重复,每一个“线程对象”对应线程的“变量副本”,而到达了线程安全。

        概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。更深入理解ThreadLocal关键词传送门!


        我是先生,找寻着那位迷路的Miss。最后,愿各位javaer,合上电脑的刹那,有着侠客收剑入鞘的骄傲!

你可能感兴趣的:(jvm内存模型)