精华内容
下载资源
问答
  • Volatile底层实现原理

    2021-04-12 09:42:31
    在java中,关键字volatile那是必须要掌握的,这在多线程并发中大量被使用。从之前的jdk源码也可以知道,volatile和CAS构成了java语言高...那么volatile是怎么实现这两个特性的呢。 可见性: 在了解可见性性之前..

    在java中,关键字volatile那是必须要掌握的,这在多线程并发中大量被使用。从之前的jdk源码也可以知道,volatile和CAS构成了java语言高并发的基石。我们一般会把volatile称为轻量级的锁,有时我们在使用volatile的时候能够达到更高的并发。那么关键字 volatile的作用是什么?我们在实际中如果需要使用volatile的话,无非是保证有序性和可见性。有序性和可见性在之前的 并发的特性中有介绍。那么volatile是怎么实现这两个特性的呢。

     

    可见性

    在了解可见性性之前,我们得熟悉几个cpu的指令编码,这样能方便我们快速理解这块的实现(图片源自java并发编程的艺术)。

     

    图片

     

    如果我们对一个变量加上volatile关键字,那么在编译成汇编的时候会加上lock前缀,那么这个lock前缀就是关键能完成这个可见性的操作,具体实现如下:

    1)将当前处理器的缓存行数据写回到系统内存

    2)这个写回内存的操作会使其他cpu里面缓存的了该内存地址的变为无效

    这两个和我们之前讲的内存模型里面是一致的,就是实现写回主存,其他内存地址无效,那么其他地址如果要读取数据的话,就必须要从主存中从新拉取数据。

     

    有序性

    指令重排序是java为了提升性能而对指令进行重新排序。指令重排序包括以下几个重排序过程:编译器重排序 --》指令集并行重排序 --》内存系统的重排序。实际上就是从代码到cpu执行的一系列过程,为了优化性能,都进行了相应的指令重排序的操作。但是在单线程的情况下,这些指令的重排序是能够保证执行的结果和实际看到代码的顺序的结果是一样的,但是在多线程的情况下 ,指令重排序就可能导致意外的“惊喜”。

    volatile在实现上是通过限制编译器重排序,指令集重排序实现的。Volatile关键字规定了编译器的重排序规则:(图片来自java并发编程的艺术)

     

    图片

     

    从这个表格上我们可以看出以下几点:

    1) 第二个操作是volatile写时,第一个操作无论是啥都不能重排序这个操作保证写前和写后的不会顺序错乱,写前的不会在写后操作。

    2) 第一个操作是volatile读时,第二个操作无论是啥都不能重排序。这个保证volatile读的顺序,读后的不会到读前面。

    3) 第一个操作是volatile写,第二个操作是volatile读时,不能重排序这个保证volatile写在读之前。

     

    Ok,当我们了解了这几条规则之后,我们JMM是怎么实现这些规则的,通过一种叫做内存屏障的保护进行,这个之前在unsafe类中也有提到loadFence,storeFence等类似内存屏障的操作。JMM在使用内存屏障采用的策略如下:

    1)在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个StoreLoad屏障,用来完成对写的保护。 

    2)在每个volatile读操作的后面插入一个LoadLoad屏障和LoadStore屏障,用来完成对读操作的保护。

     

    下面我们看一个例子:

    1. public class Test{  

    2.     volatile int v1 = 1;   

    3.     public void testVolatile() {   

    4.         int i = v1; // 第一个volatile读    

    5.         v1 = i + 1; // 第一个volatile写    

    6.         //...   

    7.     }  

    8. }  

    那么编译器生成的字节码将会如下:

     图片

    上面是编译器处理后的指令,但是有些cpu产商会把这些屏障在进一步优化,但是这个优化实际上是能够保证先后顺序的。

    虽然现在当我们了解了volatile的实现原理,但是实际上我们如果想要用volatile替换锁的话,还是要慎重考虑,因为多线程在实际生产中真的有可能 会导致很诡异的情况,这种往往要定位很久。但是如果项目对性能要求不是特别高的话,使用锁是一种比较保险且有效的方法实现多线程的同步的。

    想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈

                                                                       

    展开全文
  • volatile底层实现原理

    千次阅读 2017-07-22 12:10:07
    volatile实现原理

    定义

    java编程语言允许线程访问共享变量,为了确保共享变量能够被准确和一致的更新,线程应该通过排他锁获得这个变量。java提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到的这个变量的值是一致的。

    汇编代码

    使用命令获得汇编代码

    java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
    
    Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
    Loaded disassembler from /Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home/jre/lib/hsdis-amd64.dylib
    Decoding compiled method 0x0000000110da4b50:
    Code:
    [Disassembling for mach='i386:x86-64']
    [Entry Point]
    [Constants]
      # {method} {0x000000010f163000} 'hashCode' '()I' in 'java/lang/String'
      #           [sp+0x40]  (sp of caller)
      0x0000000110da4cc0: mov    0x8(%rsi),%r10d
      0x0000000110da4cc4: shl    $0x3,%r10
      0x0000000110da4cc8: cmp    %rax,%r10
      0x0000000110da4ccb: jne    0x0000000110ceae20  ;   {runtime_call}
      0x0000000110da4cd1: data32 data32 nopw 0x0(%rax,%rax,1)
      0x0000000110da4cdc: data32 data32 xchg %ax,%ax
    [Verified Entry Point]
      0x0000000110da4ce0: mov    %eax,-0x14000(%rsp)
      0x0000000110da4ce7: push   %rbp
      0x0000000110da4ce8: sub    $0x30,%rsp
      ......

    mac系统下使用此命令的前提是下载hsdis-amd64.dylib,并将其放入到jdk的jre下的lib目录下

    实现原理

    通过利用工具获得class文件的汇编代码,会发现,标有volatile的变量在进行写操作时,会在前面加上lock质量前缀。

    这里写图片描述

    而lock指令前缀会做如下两件事

    1. 将当前处理器缓存行的数据写回到内存。lock指令前缀在执行指令的期间,会产生一个lock信号,lock信号会保证在该信号期间会独占任何共享内存。lock信号一般不锁总线,而是锁缓存。因为锁总线的开销会很大。

    2. 将缓存行的数据写回到内存的操作会使得其他CPU缓存了该地址的数据无效。

    展开全文
  • Java内存模型及Volatile底层实现原理
  • volatile中是采用锁总线的方式实现可见性,在总线被锁期间其他CPU不可以访问主内存中变量,这种方式效率太低。 现在的可见性是由java和CPU共同完成的,利用CPU的缓存一致性来保证可见,原理如下: 要想理解可见性...

    三大特性:1.保证可见性 2.不保证原子性 3.防止指令重排

    保证可见性原理

    在volatile中是采用锁总线的方式实现可见性,在总线被锁期间其他CPU不可以访问主内存中变量,这种方式效率太低。
    现在的可见性是由java和CPU共同完成的,利用CPU的缓存一致性来保证可见,原理如下:
    M
    要想理解可见性原理首先得知道JMM的8个原子操作,如下图所示红色字体

    • 首先线程B修改了flag的值,并传递给工作内存
    • 工作内存将flag标记为M(Modify)状态,并发送一个消息给总线
    • A将会通过总线嗅探机制发现变量修改消息,此时A中的flag将会被置为无效状态 I(invalid)
    • A重新读取主内存中的变量,以达到同步。
      在这里插入图片描述
      变量状态
      在这里插入图片描述

    不保证原子性的原因

    在上图中,当线程B修改了变量值后线程A中的就失效了,也就是说,不论A中执行器对变量做了什么操作都会被视为无效,而重新从主存中读取变量值,也就是说A的操作不是原子性操作。

    防止指令重排

    指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序.
    防止指令重排的方式就是加上内存屏障,只要加了volatile的就会在前后加上内存屏障
    在这里插入图片描述

    展开全文
  • Synchronize和Volatile底层实现原理

    千次阅读 2019-01-14 22:49:19
    本文主要是关于第二章Java并发机制的底层实现原理的相关记录。主要包括volatile,synchronized,原子操作等实现原理的分析。 点击阅读更多系列文章我的个人博客–>幻境云图 1. 上下文切换 多线程 ...

    最近在看并发编程的艺术这本书,希望加深对并发这块的理解。毕竟并发相关还是十分重要的。本文主要是关于第二章Java并发机制的底层实现原理的相关记录。主要包括volatile,synchronized,原子操作等实现原理的分析。

    更多文章欢迎访问我的个人博客–>幻境云图

    1. 上下文切换

    多线程

    即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。

    什么是上下文切换

    CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。

    所以任务从保存到再加载的过程就是一次上下文切换。

    上下文切换也会影响多线程的执行速度

    因为线程有创建和上下文切换的开销,所以有时候并发不一定比串行快。

    减少上下文切换的办法

    减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。

    • 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一
      些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
    • CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
    • 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这
      样会造成大量线程都处于等待状态。
    • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

    2. volatile关键字

    如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

    有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,其中就包括了Lock前缀.Lock前缀的指令在多核处理器下会引发了两件事情。

    1)将当前处理器缓存行的数据写回到系统内存。

    Lock前缀指令导致在执行指令期间,声言处理器的LOCK#信号。在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存。

    如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反,它会锁定这块内存区
    域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁
    定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

    2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

    处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。

    3. synchronized原理与应用

    Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。

    Java中的每一个对象都可以作为锁。具体表现
    为以下3种形式。

    • 对于普通同步方法,锁是当前实例对象。
    • 对于静态同步方法,锁是当前类的Class对象。
    • 对于同步方法块,锁是Synchonized括号里配置的对象。
      当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

    3.1 底层实现

    JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。

    代码块同步是使用monitorentermonitorexit指令实现的.

    方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。

    monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

    3.2 Java对象头

    synchronized用的锁是存在Java对象头里的。

    java的对象头由以下三部分组成:

    1,Mark Word

    2,指向类的指针

    3,数组长度(只有数组对象才有)

    3.3 锁的升级与对比

    Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状
    态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。

    偏向锁

    HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

    当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

    偏向锁解除

    偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程.

    轻量级锁

    (1)轻量级锁加锁
    线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
    (2)轻量级锁解锁
    轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

    优缺点比较

    4. 原子操作的实现原理

    原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。

    4.1 处理器如何实现原子操作

    处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。

    第一个机制是通过总线锁保证原子性。

    所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。

    第二个机制是通过缓存锁定来保证原子性。

    总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。

    所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效.

    4.2 Java如何实现原子操作

    使用循环CAS实现原子操作

    JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

    CAS实现原子操作的三大问题

    1.ABA问题

    但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

    ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1。

    2.循环时间长开销大

    自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

    3.只能保证一个共享变量的原子操作

    操作多个共享变量时无法使用CAS操作,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。

    使用锁机制实现原子操作

    锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。

    参考

    本文内容来自Java并发编程的艺术

    展开全文
  • java基础---volatile底层实现原理详解

    千次阅读 2020-04-01 01:15:07
    想知道volatile实现原理首先得去了下解JMM,我们都知道JVM会为每一个thread开辟一块自己的工作空间,在我们操作变量时是从主内存拿到变量的一个副本,然后对副本进行操作后再刷新到主内存中这么一个总体的流程。...
  • volatile底层实现原理和其应用

    千次阅读 2018-11-27 06:42:06
    volatile时轻量级的synchronized,它在多处理器开发中保证了数据的读的一致性,意思就是当一个线程修改一个共享变量时,另外一个线程能读到这个共享变量的值。如果volatile变量修饰符使用的恰当的话,他的运行成本会...
  • 当共享变量被声明为volatile后,对这个变量的读/写操作都会很特别,下面我们就揭开volatile的神秘面纱。 1.volatile的内存语义 1.1 volatile的特性 一个volatile变量自身具有以下三个特性: 可见性:即当一个...
  • volatile实现原则: ·Lock前缀指令会引起处理器缓存回写到内存。(总线锁、缓存锁) ·一个处理器的缓存回写到内存会导致其他处理器的缓存无效。(MESI、嗅探) 缓存一致性协议: 处理器上有一套完整的协议,来保证...
  • volatile的定义与实现 Java编程语言允许线程访问共享变量,为了确保共享变量能够准确和一致的更新,线程应该通过排他锁单独获取这个变量。如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量...
  • volatile关键字及底层实现原理 其实 Java JMM 内存模型是围绕并发编程中原子性、可见性、有序性三个特征来建立的,关于原子性、可见性、有序性的理解如下: 原子性: 就是说一个操作不能被打断,要么执行完要么不...
  • 15.深入分析Volatile的实现原理 14.java多线程编程底层原理剖析以及volatile原理 13.Java中Volatile底层...11.volatile底层实现原理 =================== 15.深入分析Volatile的实现原理 引言 在多线程并发编...
  • Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU指令。 在多线程并发编程中synchronized和...
  • volatile底层工作原理

    2021-06-17 23:16:46
    Volatile缓存可见性实现原理 底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(存行锁定)并回写到主内存 IA-32架构软件开发者手册对lock指令的解释: 1)会将当前处理器缓存行的数据立即写回到系统...
  • 加了volatile后,编译的汇编语言中多了LOCK指令,那么这个指令的作用是什么呢? LOCK指令作用 将当前缓存行的内容写回到内存中,这个写回内存的操作会使得其他cpu里...因此volatile能够实现线程间数据的可见性。...
  • volatile关键字在Java中多线程编程中作为必不可少的关键字,它的作用和原理你知道多少?在我们线程之间通信有很多种方式,它主要是作用在什么方式中呢?在这种通信方式中它是通过什么方式来实现线程之间的数据...
  • volatile底层原理实现

    万次阅读 2020-10-24 13:01:55
    volatile底层原理 volatile的两个作用: 可见性 防止指令重排序 计算机的组成 下图是一个典型的计算机结构图,计算机的组成主要包括CPU、存储器(内存)、IO(输入输出设备)。 存储器的层次结构 下图是计算机...
  • volatile底层原理详解

    2021-06-02 16:01:56
    volatile底层原理详解volatile底层原理详解1、volatile的作用1.1、volatile变量的可见性1.2、volatile变量的禁止指令重排序2、volatile的的底层实现2.1、volatile 有序性实现2.2、volatile 禁止重排序2.3、volatile ...
  • 多线程并发之volatile底层实现原理

    千次阅读 2018-12-28 10:29:53
    本篇博文着重学习volatile底层实现原理。 【1】回顾volatile volatile相当于是轻量级的synchronized。如果一个变量使用volatile,则它比使用synchronized的成本更加低,因为它不会引起线程上下文的切换和调度。 ...
  • 1.并发编程领域的关键问题 1.1 线程之间的通信 ...线程的通信是指线程之间以何种机制来交换信息。在编程中,线程之间的... 3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 785
精华内容 314
关键字:

volatile底层实现原理