码迷,mamicode.com
首页 > 编程语言 > 详细

Java关键字---volatile

时间:2019-08-29 12:04:19      阅读:97      评论:0      收藏:0      [点我收藏+]

标签:变量定义   main   xom   tap   ie7   ota   mcc   lov   rda   

一、计算机中线程不安全问题产生原因

    计算机在执行程序时,每条指令都是在CPU中执行的,执行的过程会涉及到读取和写入。程序运行过程中的临时数据是存放在主存(物理内存)中的,这就会产生一个问题,由于CPU的执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU的执行速度相比就慢很多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,就会大大降低指令的执行速度。
    为了解决这个问题,计算机中就有了CPU缓存的概念。在程序的运行过程中,操作的数据会从内存中复制一份到CPU缓存中,当CPU进行计算的时候就可以直接从它的缓存中读取数据和写入数据,当运算结束之后,再将缓存中的数据刷新到主存中。举个例子:
 
 t = t + 1;
   
  CPU在执行这段代码时,会先从高速缓存中查看是否有t的值,如果有,则直接拿来使用,如果没有,则会从主存中读取,读取之后会复制一份存放在高速缓存中方便下次使用。之后cup进行对t加1操作,然后把数据写入高速缓存,最后会把高速缓存中的数据刷新到主存中。
    这一过程在单线程运行下时没问题的,单当在多线程的情况下就会有问题了。在多核CPU中,每条线程可能会运行在不同的CPU中,因此每个线程运行时有自己的CPU缓存。这时就会出现同一个变量在两个CPU缓存中值不一致的情况。
    例如,两个线程分别读取了t的值,假设此时t的值为0,并且把t的值存到了各自的CPU缓存中,然后线程1对t进行了加1操作,此时t的值为1,并把t的值写回到了主存中。但线程2中的CPU缓存还是0,进行加1操作后,t的值为1,再把t写回主存,此时就出现了线程不安全的问题。
技术图片技术图片
对非普通变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
    

二、如何确保线程安全

    Java中有几种机制来确保线程安全,例如Volatile、Synchronized关键字,这些机制适用于各种平台。通常,要保证线程安全,就是指要保证以下三个方面特性的完整和正常:
    
    1.原子性---在对数据进行操作时这个操作时不可分割的,比如a=0这个操作就是个原子操作,a++(实际上是a=a+1)这个操作是可分隔的,它就不是一个原子操作。java中使用synchronized、lock、unlock来保证原子性。
    
    2.可见性---一个线程对主内存的修改能及时被其他线程看到,也就是说,在多线程环境下,某个共享变量如果被其中一个线程给修改了,其他线程能够立即知道这个共享变量已经被修改了,当其他线程要读取这个变量的时候,最终会去内存中读取,而不是从自己的工作空间中读取。
    比如:使用volatile修饰的变量就具有可见性,volatile修饰的变量不允许内部缓存和指令重排序,即它的操作不经过CPU缓存,而直接修改内存。volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过线程--->工作空间--->主内存这样的顺序来完成。
技术图片技术图片
    
    3.有序性---java使用volatile和synchronized来保证线程之间的有序性,volatile是因为本身包含“禁止执行重排序”的语义,synchronized是由“一个变量在同一时刻只允许一个线程对其进行lock操作”获得的。产生这个问题原因是虚拟机在执行代码时,并不会按照我们事先编写好的代码顺序来执行,比如下面两行代码:
int a = 1;
int b = 2;
    对于这两行代码,无论是先执行a=1还是b=2,都不会对a,b最终的值有任何影响,所以虚拟机在编译的时候,是有可能对它们重排序的。CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理的机制,假如执行int a = 1需要100ms的时间,而执行int b = 2 需要1ms的时间,并且执行哪句代码都不会对a,b的最终值产生影响,那当然是先执行b=2这句代码了。所以,虚拟机在进行代码编译优化的时候,对于那些改变顺序之后不会对最终变量的值造成影响的代码,是有可能将他们进行重排序的。
    那么这个时候指令的重排序虽然对值没有什么影响,但可能会出现线程安全的问题。
public class NoVisibility {
    private static boolean ready;
    private static int number;
    private static class ReaderThread extends Thread {
        @Override
        public void run() {
            while(!ready) {
                Thread.yield();
            }
            System.out.println(number);
        }
    }
    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}
    
  这段代码最终打印的一定是42吗?如果没有重排序的话,打印的确实会是42,但如果number = 42和ready = true被进行了重排序,颠倒了顺序,那么就有可能打印出0了,而不是42。(因为number的初始值会是0)。在没有同步的情况下,编译器、处理器在运行时都可能对操作的执行顺序进行一些意向不到的调整,我们也无法确定代码的实际执行顺序。
    如果一个变量被声明成volatile的话,那么这个变量不会被重排序,也就是说,虚拟机会保证这个变量之前的代码一定比它先执行,而之后的代码一定比它后执行。例如把上面中的number声明为volatile,那么number = 42一定会比ready = true先执行。不过需要注意的是,虚拟机只是保证了这个变量的执行顺序,而它之前或之后的代码执行顺序还是有可能会进行重排序的。
 

三、volatile原理

    Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
  在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
    当一个变量定义为 volatile 之后,将具备两种特性:
      1.保证此变量对所有的线程的可见性,这里的“可见性”,如之前所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。
  2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
 

四、volatile 性能:

  volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
 
 

Java关键字---volatile

标签:变量定义   main   xom   tap   ie7   ota   mcc   lov   rda   

原文地址:https://www.cnblogs.com/sxhjoker/p/11428477.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!