美文网首页
JVM synchronized和volatile 关键字

JVM synchronized和volatile 关键字

作者: 真海ice | 来源:发表于2018-02-28 12:29 被阅读0次

本编文章都是基于下图这个,计算机cpu 、缓存、内存、线程之间的关系;


无标题.png

一、缓存一致性问题

计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

中间的高速缓存就是cpu和内存的中间过程。
但是在多线程,每个线程在不同的cpu中运行时,每个线程分别读取内存中的值存入各自所在的CPU的高速缓存当中,cpu对数据改变后,就造成了缓存一致性的问题,通常称这种被多个线程访问的变量为共享变量。

也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

为了解决缓存不一致性问题,通常来说有以下2种解决方法:

1)通过synchronized锁的方式
2)通过缓存一致性协议

二、并发编程中的三个概念

  • 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,相当于事物的概念。

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

  • 有序性:即程序执行的顺序按照代码的先后顺序执行,因为有指令重排序问题;
    指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。这种情况在单线程下没有问题,但是在多线程下有可能出现问题。

也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

三、synchronized

同步锁可以保证并发编程中的原子性、可见性、有序性;但是效率比较低。

四、volatile

  1. 保证可见性问题:
    一个共享变量被volatile修饰时,当CPU对该变量有写操作时,它会保证修改的值会立即被更新到主内存中,并会发出信号通知其他CPU将该变量的缓存设置为无效状态,因此当其他CPU需要读取这个变量时,发现自己的高速缓存中该变量是无效的,那么它就会从内存重新读取。

    而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

    可见性只能保证每次读取的是最新的值

  1. 保证有序性问题:
    当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

    在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行

  2. 不能保证原子性问题:
    测试代码:

package com.test.jvm;

public class Test {
  
  public volatile int i = 0;  
  public void increase(){   //可以添加关键字synchronized看结果不同
      i++;
  }
  
  public static void main(String[] args) {        
      final Test test = new Test();
      for(int x =0; x<10; x++){
          new Thread(){@Override
              public void run() {
                  for(int y=0; y<1000; y++){
                      test.increase();
                  }
              }
          }.start();
      }
          
      while(Thread.activeCount()>1){  //保证前面的线程都执行完
          Thread.yield();
          System.out.println(test.i);
       }      
  }

}

最后i的结果并不是10000 ,总是小于10000;
解释:
假如某个时刻变量 i 的值为10,

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

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

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

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

解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。但是要注意,线程A对变量进行读取操作之后,被阻塞了的话,并没有对 i 值进行修改。然后虽然volatile能保证线程B对变量 i 的值读取是从内存中读取的,但是线程A没有进行修改,所以线程B根本就不会看到修改的值。

总结:

使用volatitle关键字要保证的两个条件:
1) 对变量的写操作不依赖于当前值
2) 该变量没有包含在其他变量中

相关文章

网友评论

      本文标题:JVM synchronized和volatile 关键字

      本文链接:https://www.haomeiwen.com/subject/ynqbxftx.html