深入理解Java虚拟机--java内存模型与线程

        Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递均需要通过主存来完成。

      1.内存间交互操作

       关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实验细节,Java内存模型中定义了8种操作来完成:

  • lock(锁定):作为主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作为主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个命令。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
      2.对于volatile型变量的特殊规则
        关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义成volatile之后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量做不到这一点,变量的数值在线程之间传递需要通过主内存来完成。如:线程A修改了一普通变量的值,然后向主内存进行回写,另外一条线程B在线程A写完了之后再从主内存进行读取操作,新变量的值才会对B线程可见。
        由于volatile变量只能保证可见性,但java里面的运算并非原子操作,导致volatile变量的运算在并发下不是安全的。在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。
    (1)运算结果并不依赖变量的当前值(否则就和value++类似了),或者能够确保只有单一的线程修改变量的值。
    (2)变量不需要与其他的状态变量共同参与不变约束。比如start和end变量都被声明为volatile, 并且start和end组成不变约束start
深入理解Java虚拟机--java内存模型与线程_第1张图片
       因此,在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,当变量的值由自身的上一个决定时,如n=n+1、n++等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m+1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。        
       第二个特性是禁止指令重排序优化。普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,这样就是Java内存模型中描述的所谓的“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics )。
       Java内存模型中对volatile变量定义的特殊规则:
  • 在工作内存中,每次在使用volatile变量前都必须先从主内存刷新最新的值,用于保证能看见其他线程对该变量所做修改后的值。
  • 在工作内存中,每次修改volatile变量后都必须立刻同步回主内存中,用于保证其他线程能看到自己对该变量所做的修改。
  • volatile变量不会被指令重排序优化,保证代码执行顺序与程序的顺序相同。
       另外,除了volatile之外,synchronized和final也能实现可见性,同步块的可见性是由:“对一个变量执行unlock操作之前,必须先把此变量同步回主内存”。而final可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this引用传出去,即发生this逃逸,那么在其他线程中就能看见final字段的值。
       这里插一下,所谓线程安全是指:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形。
       
        3.对于long和double型变量的特殊规则
        Java内存模型要求lock、unlock、read、load、assign、use、store和write这8个操作都具有原子性,但是对于64位的数据类型long和double,在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read和write这四个操作的原子性,这点就是所谓的long和double的非原子性协定。
        如果有多个线程共享一个未被声明为volatile的long或double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读到一个既非原值,也非其他线程修改值的代表了“半个变量”的数值。
        不过这种读取到“半个变量”的情况非常罕见,因为Java内存模型虽然允许虚拟机不把long和double变量的读写实现成原子操作,但允许迅疾选择把这些操作实现为具有原子性的操作,而且还“强烈建议”虚拟机这样实现。目前各种平台下的商用虚拟机几乎都选择吧64位数据的读写操作作为原子操作来对待,因此在编码时,不需要将long和double变量专门声明为volatile。



你可能感兴趣的:(Java学习,JVM精华总结)