为什么volatile关键字保证不了线程安全

    在当前高并发的时代,不懂一点高并发多线程都不好意思出去,即使没地方使用,网上大多数相关文档博客也都讲解了这些部分。

    我并不想具体介绍什么是volatile,我写这篇博客目的是说明白为什么volatile保证不了线程安全。想要线程安全必须保证原子性,可见性,有序性。而volatile只能保证可见性和有序性

    在说明这个问题之前,首先还是要说明下cpu和内存,cpu和内存直接是有高速缓存的,一般分为多级。cpu首先是要从内存中读取一个数据进缓存,然后从缓存中读取进行操作,将结果返回给缓存,再把缓存写回内存。

    如果同一个变量i=0,有两个线程执行i++方法,线程1把i从内存中读取进缓存,而现在线程2也把i读取进缓存,两个线程执行完i++后,线程1写回内存,i = 1,线程2也写回内存i = 1,两次++结果最终值为1,这就是著名的缓存一致性问题。为了解决这个问题,前人给了两种方案

    总线锁

    缓存一致性协议

    cpu为了和各个硬件打交道方便,设计师们把每个硬件都连接一个线到cpu,但是发现这样太麻烦了,所以改为所有硬件都挂在总线上,cpu通过总线和各个硬件打交道。如果使用总线锁,就阻塞了其他cpu和其他硬件交互(内存之类,磁盘,等等),i++这条语句就必须执行完了,其他cpu才能执行,否则只能一个cpu去和硬件交互。这也是一种解决办法,问题也明显,特别效率低下。

    为了解决这个问题提出缓存一致性协议,具体协议就不讲,简单解释一下,如果我写入之后发现这是共享变量就使得其他cpu缓存了的值失效,让它再次去内存中读取。


下面这段话摘自《深入理解Java虚拟机》:

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。


    解释一下这段话的内容,重排序的指代码的真正执行过程可能不是代码书写的顺序,这是为了是cpu流水线作业提高cpu的利用率而优化的一门技术。

    而lock前缀指令(内存屏障),一个屏障会把这个屏障前写入的数据刷新到内存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。

    这样咋一看貌似可以保证线程的安全性呀,为啥不能保证呢

    这样如果有一个变量i = 0用volatile修饰,两个线程对其进行i++操作,如果线程1从内存中读取i=0进了缓存,然后把数据读入寄存器,之后时间片用完了,然后线程2也从内存中读取i进缓存,因为线程1还未执行写操作,内存屏障是插入在写操作之后的指令,意味着还未触发这个指令,所以缓存行是不会失效的。然后线程2执行完毕,内存中i=1,然后线程1又开始执行,然后将数据写回缓存再写回内存,结果还是1。

 

    

猜你喜欢

转载自blog.csdn.net/qq_33330687/article/details/80990729