我要投搞

标签云

收藏小站

爱尚经典语录、名言、句子、散文、日志、唯美图片

当前位置:港彩神鹰 > 读入原语 >

java中关于volatile的理解疑问?

归档日期:04-30       文本归类:读入原语      文章编辑:爱尚语录

  尽管volatile关键字保证了所有对于volatile变量的读取都会直接调用主存,而所有对于volatile的写入都会直接写到主存,但这里面依然有些情况单单声明volatile变量是不够的。

  在我们之前提到的情况里,当只有Thread 1写入到共享的counter变量,声明counter变量为volatile足够保证线程B看到的counter变量总是最新的。

  但是事实上,多线程会写入到同一个共享的volatile变量了,如果对于这个volatile变量的写入不是基于他目前的值,换句话说,如果线程写入到一个共享的volatile变量不会首先去读取它的值去弄清楚它接下来的值。

  当一个线程需要去第一次读取一个volatile变量,同时生成一个基于这个变量值的变量时(i = i + 1),这个volatile关键字就不够来保证其的可视性了。在这短短的读取、写入volatile变量的间隔中,当多个线程可能回去读取这同一个值的时候,就会建立一个竞争的环境,在一个线程为这个变量生成一个新的值,和写入这个值到内存中这个过程中,可能就会把其他人的值给复写了。

  在多线程计数的情况下,volatile关键字变量是完全不够的,接下来的例子会讲一些细节的东西

  想象如果线读取了共享的counter变量value 0到他的CPU cache中,增加其到1但还没来得及写回到主存中,线也可以从主存中读取同一个counter变量(此时这个变量还是0),线也可以令这个counter从0变成1,也还没来得及写回主存。

  线和线现在出现了不同步的现象,counter的线,但是每一个线程的counter都变是1(存于CPU cache中),而主存中的变量甚至还是0。是不是很恐怖!尽管每个线程都会直接把他们的counter值写回到主存中,但是这个counter的值依然是错误的。

  关于缓存,其实缓存一致性协议从硬件上保证了内存跟缓存的一致性,从程序的角度看两者可以认为是一致的。CPU的写缓冲(store buffer)倒是会产生不一致,但这个于缓存是不同的概念。x86处理器唯一会产生内存访问乱序就是写后读,其原因就是其写内存指令执行完后只是写到了store buffer缓冲区就开始执行下一条指令,而由store buffer异步将数据写回内存。如果下一条指令是读内存,又缓存命中,那在其它CPU上就可能看到读内存的提前了。

  关于volatile的可见性,JMM规范里只有一条,要求同一个线程里的所有同步原语(lock,unlock,volatile读写 ,开启线程,线程开始执行等)是全序(total order)关系,即这些同步原语之间不能乱序执行,其它线程看到的顺序必须是一致的。这里并没有说一个线程的volatile写能被其它线程马上看到,事实上也做不到,没有哪个系统能做到这种严格一致性模型。事实上只要最终能看到,且看到的顺序一致,就不会影响程序的正确性,这也就是顺序一致性模型。

  Java的内存模型定义了Java虚拟机如何和计算机物理内存进行交互。Java虚拟机是一体化的计算机模型,所以它自然也包含了内存模型。

  如果你要设计运行稳健的并发程序,理解Java内存模型是非常必要的。Java内存模型定义了不同线程之间变量读写的可见性以及如何同步访问共享变量。

  最原始的Java内存模型设计的很不足。后来到了Java1.5版本就重新制定了,这个版本的内存模型一直延续到了现在。

  Java内存模型内部分为线程堆栈和堆两种区域。下图粗略了表示了这种结构。

  Java虚拟机中的每个线程都有它自己的线程栈。线程栈记录了当前线程到达当前执行点所经历的一系列方法调用信息。暂且把它称之为【调用栈】。随着线程不停地执行代码,调用栈一直不停的改变。

  线程栈记录了正在运行的调用栈里所有方法的局部变量信息。线程只能访问它自己的线程栈。局部变量仅对创建它的线程有效,其它线程是完全看不见的。甚至当两个线程执行的是同一段代码逻辑,它们也会分别在自己的线程栈里创建这些局部变量,每个线程都有它自己的局部变量,互不干扰。

  所有原生类型(boolean, byte, short, char, int, long, float, double)的局部变量都是完全存储在当前的线程栈里面,别的线程完全看不到。线程之间可以通过拷贝来分享原生类型的变量,但是无法直接共享。

  堆包含了Java应用程序创建的所有对象,不管这些对象是被哪个线程创建的。这些对象包含了所有原生类型的包装类型。无论这些被创建的对象是被赋值给了哪个局部变量还是挂在了某个对象的成员变量上,它们都存储在堆里面。

  下图展示了Java虚拟机里面的调用栈和局部变量以及存在堆里面的所有对象。

  局部变量也可以是对象的引用,这种情况下对象的引用也就是局部变量本身是存在栈里面的,而对象本身的内容则放在堆里面。

  对象里有方法,这些方法又有局部变量。这些局部变量也存在栈里面,尽管这些方法所属的对象是放在堆里面的。

  对象的所有字段同对象一样都是放在堆里的,不论这个字段是原生类型还是对象的引用类型。

  堆里的对象可以被所有线程访问,只要这些线程有相应对象的引用。线程如果可以访问一个对象,那一定就可以访问这个对象的成员字段。如果两个线程同时访问同一个对象的同一个方法,那么它们会同时访问对象的字段,当时每个线程都会创建属于自己的局部变量。

  两个线程都有一些局部变量。其中局部变量2指向了堆里面的对象3。这两个线程都有属于自己的指向同一个对象的引用变量。这些引用都是局部变量,都存放在各自的线程栈上,尽管它们都指向同一个对象。

  注意到这个共享的对象3持有对象2和对象4的引用作为它的内部成员。通过成员变量引用, 这两个线。

  图中两个线分别指向了堆里的不同对象。理论上堆里的所有对象都可以被两个线程访问,但是就图上所示,每个线程都只有一个对象的引用。

  methodOne定义了一个原生的局部变量和一个对象引用局部变量。两个线程在执行methodOne的时候都会在各自的栈上创建局部变量1和局部变量2,它们互不干扰,一个线程是看不到另外一个线程对局部变量做的任何修改的。

  两个线程在执行methodOne时还会创建各自的局部变量2,然后这两个变量都指向堆里的同一个对象。代码设置局部变量2指向一个静态变量引用的对象,这个静态变量是唯一的,也存放在堆里。结果就是这两个局部变量都指向同一个又静态变量指向的ShareObject对象,ShareObject这个对象实例就存放在堆里,就是图中的对象3。

  注意到MyShareObject的两个成员变量都是long类型的。因为它们是成员变量,所以也跟对象一起放在堆里。只有局部变量才会放在栈上。

  现代的硬件内存结构和Java内存模型是不一样的。想要理解Java内存模型是如何同物理内存交互的话,先把硬件内存结构搞懂非常重要。这一节我们来看看硬件内存结构,接下来我们再看Java内存模型是如何与之交互的。

  现代计算机一般都有多个CPU,每个CPU又有多个核。所以它们往往可以同时跑多个线程。每个CPU同一时间都可以跑一个线程。这就意味着如果你的Java程序是多线程的,每个CPU都会同时跑着一个线程。

  每个CPU内部都有一堆基本的寄存器。CPU和这些寄存器交互要比和内存交互快得多。

  每个CPU都有一个缓存层,现代的CPU一般都是特定大小的缓存层。CPU访问这些缓存要比访问内存快,但是还是比不上寄存器。有些CPU可能会有多个缓存层,不过这不影响我们理解Java内存模型是如何同内存进行交互的。我们只要知道CPU有这样一个缓存层就够了。

  当CPU要读主内存时,它会首先读取一小块内存到CPU缓存里,接着又会读取缓存的一小块到寄存器里,然后就可以继续操作了。当CPU要写主内存时,它会将寄存器里的值Flush到缓存里,后面又会将缓存里的值Flush到主内存。

  当CPU想用这些缓存存点别的什么东西时会先把缓存里的数据会刷回到主内存。CPU缓存可以在从主存加载数据的同时回写数据到主存,这两个操作对象是CPU缓存的不同部分。CPU缓存没必要一次性读写整个缓存区域。一般来说CPU缓存更新的单位是【缓存行】,英文是cache lines。一部分缓存行从主存加载数据,另一部分缓存行回写数据到主存。

  前面提到,Java内存模型和硬件内存结构是不一样的。硬件内存结构是不会区分线程栈和堆的。在硬件上,线程栈和堆都放在主内存里,还有一些甚至会在CPU缓存和寄存器里。如下图所示

  如果多个线程共享一个对象,而这个对象又没有使用volatile修饰也没有任何同步控制的话,一个线程对共享变量的修改是可能不会立即被其它线程看到的。

  试想一开始对象是在主存里创建的,然后CPU将对象加载到CPU缓存,在缓存里修改了这个对象而没有立即将缓存写会主存。那这个修改过的对象是不能够被运行在其它CPU上的线程看到的。结果就是每个线程都有这个对象的一个拷贝,分别放在不同的CPU缓存里。

  下图能很好的表现这种情况。运行在左边CPU上的线程将count变量加载到CPU缓存中,然后将count修改成了2,而右边CPU上跑的线程则看不到这种改变。因为修改过的缓存行没有同步刷回主内存。

  为了解决这个问题就必须用到volatile关键字,volatile关键字会确保读操作会直接读取主内存,对变量的修改也是立即回写主内存。

  多个线程共享一个对象,并且同时对这个对象进行修改的时候,竞态条件就产生了。

  假设线程A将共享变量count读到CPU缓存,同时线程B也将共享变量count读到另一个不同的CPU缓存,然后同时对count进行加1操作,如此count变量被更新了2次。

  如果这两个加1操作是串行执行的话,那么当count变量回写内存的时候,值就是value+2

  可现在这两个操作没有进行适当的同步控制,也就是说不是串行的。不管是哪个线程先回写主存,最后的值总是value+1,而不是value+2。下面这张图显示了这种竞态条件。

  解决这种问题,你可以使用Java的synchronized关键字。synchronized关键字保证同一时间只有一个线程可以进入临界区。同步块同样也可以保证块内的读变量都是直接读主存,变量修改在退出同步块的时候会立即回写到主存,不管这个变量有没有使用volatile修改都一样。

  先说结论,题主说的情况不适用volatile,这种时候应该用synchronized。volatile大部分使用场景都是没有频繁读写的情况。

  手机强答不贴代码。volatile实际上对变量是不加锁的。volatile的工作原理是当变量被更新时会立即写回内存,使cpu缓存失效。

  在多核处理器同时处理某变量时,假设核A更新这个变量的值,在更新前不会去取自己缓存的值,而必须重新读取内存中变量值。然而如果多线程频繁读写中使用volatile,很显然需要互斥锁。没有锁的情况下,会在核A更新内存时,核B取不到核A更新变量的结果,在A更新内存之前读取到旧的值,并在A更新之后,又更新了内存中这个值。加上类似读写的互斥锁后明显可以解决这种问题。

  所以,如果你频繁读写,建议不用volatile。如果你多线程共享标志位,可以使用。

  ①首先,把变量声明为volatile 类型的,编译器与运行时都会注意到这个变量时共享的,因此不会将该变量上的操作与其他内存操作一起进行重排序(编译器为了提高效率,改变程序猿编程时给出的代码先后顺序)。

  ②其次,volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方(现在都是多核处理器,如果其中一个处理器操作变量后将其存在自己的缓存中,则该变量对于其他处理器是不可见的),因此在读取volatile变量时总会返回最新写入的值(注意,这里说的是,会读取最新写入值,即是xyzZ回答中,某一个线程执行完成。

  ③写回内存之后的值,如果一个线程读取时,另一个线程修改的值还没执行到写入操作,则这个值对于其他线程在此刻是不可见的)。

  在volatile修饰的变量下的每个线程在寄存器中还是会有自己的缓存行做上次最新值的保留,但是当此变量被更新时,会将其他线程下的缓存行失效,并回写到主内存,强制其他线程再访问此变量时从主内存中获取,达到可见性的效果,

  另外volatile修饰的变量相关的操作会禁止指令重排序,也是为了保证可见性可以正常生效,具体请见

  volatile通过保证对变量的读或写都是直接从内存中读取或直接写入内存中,保证了可见性;但是volatile并不足以保证线程安全,因为无法保证原子性,如count++操作:

  不是volatile的变量的指令执行顺序是1-2-3;而声明为volatile的变量,顺序是1-23。从这里看,volatile保证了一个线程修改了volatile修饰的变量,变化会马上体现在内存中。线程间看到的值是一样的。

  上面说了无法保证原子性是指:多核cpu,线操作,结果写入寄存器同时flush到内存;随后B也执行了同样的操作。count本来应该的结果是加2,但是却只加了1。原因就是我们通常所指的读和写不是原子操作。我们最希望看到的是123同时执行,手段就是sychronized或者current包中的原子数据类型。

  比如int i= 0;两个线程同时进行i++操作,可以会有并发问题,因为线读取i 然后线读取i,再然后线操作,线操作,这时就是两个线两个线这个值放回去,i就等于1了,这就是多线两个线程都把i拿到cpu缓存区拷贝了一份,所以他们是分别对各自的i进行的操作,这就叫线程缓存.

  volatile所修饰的变量不具备线程缓存,所以多个线程同时操作,也只会操作内存中的那个i,一个线程修改了i的值 其他线程可以立刻见到新值,这就是可见性.

  至于指令重排序,CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理,这就是重排序,简单点说大概就是会让代码运行出现一些不可预计的顺序干扰.

  在Java编程中时常会遇见名叫volatile的变量,volatile的英文本意意为“不稳定的、易变的”,在Java中它充当了类型修饰符的角色,其用途在于修饰被不同线程访问和修改的变量。它具有可见性,在对这一变量的读写过程中,主存中的值会被读取或被刷新。但如果讨论它是否具有原子性,这则要视情况而定。

  题主异议的括号部分貌似跟我理解里的不一样。volatile应该还是有工作内存的拷贝的,只是由于volatile变量特殊的操作顺序性,看起来如同直接在主内存中读写一般。原因好像是加入了内存屏障指令,后续的所有操作必须在这个volatile变量Store之后,才能继续运。

  java内存模型中,volatile变量有一个规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。

本文链接:http://chuyenchame.com/duruyuanyu/192.html