JVM——线程同步机制

JVM——线程同步机制 JVM线程同步机制1、Mark Word1.1、Mark Word详解1.2、hashCode验证1.3、轻量级锁的状态信息1.4、重量级锁的状态信息2、synchronized设计原理2.1、synchronized的使用2.2、synchronized的具体设计3、synchronized源码分析3.1、ACC_SYNCHRONIZED解析过程3.2、monitorenter指令解析过程3.3、monitorexit指令解析过程3.4、锁获取实现过程3.5、锁释放实现过程4、volatile实现原理4.1、实现原理概述4.2、getfield指令实现过程4.3、putfield指令实现过程5、volatile伪共享6、CAS硬件同步原语6.1、CAS硬件原语6.2、JVM CAS实现6.3、ABA问题7、Unsafe功能介绍7.1、操作内存7.2、获取字段内存偏移量7.3、普通字段的读取与赋值7.4、volatile字段的读取与赋值7.5、CAS操作能力7.6、线程阻塞与唤醒7.7、内存屏障8、Unsafe实现原理8.1、volatile字段读取8.2、volatile字段写入8.3、CAS操作能力8.4、线程阻塞与唤醒9、LockSupport实现原理9.1、Unsafe初始化9.2、无阻塞对象方法9.3、有阻塞对象方法9.4、线程唤醒Java提供了3种基础的线程同步方式synchronized、volatile与CAS(Compare And Swap)硬件原语。synchronized是通过内置的对象锁来实现线程间的同步具备原子性、可见性、有序性。volatile是一种轻量级同步机制它只保证了可见性与有序性但无法保证原子性。CAS采用了无锁的原子操作来实现线程同步避免加锁带来的笨重性。1、Mark Word在JVM中Java对象是用OOPOrdinary ObjectPointer普通对象指针表示的。OOP的数据结构可以分为两部分一部分是对象的基本信息另一部分是对象的属性信息。OOP对应的实现类是oopDesc具体定义 如代码所示。oopDesc内部主要包含markWord、Class指针、对象数据、对齐补充四个部分。markWord是对象的数字化标识主要是为对象比较、垃圾回收、并发控制的功能服务的。Class指针是指向对象的Class其对应的元数据对象内存地址。对象数据包括了对象的所有成员变量其大小由各个成员变量的大小决定。比如byte和boolean是1字节short和char是2字节int和float是4字节long和double是8字节reference是4字节。对齐补充确保对象的数据大小能够被8整除如果不能被8整除padding则补齐占用空间使之能被8整除。这样做可以提高内存寻址的效率。1.1、Mark Word详解从下面代码中可以看出Mark Word的值是64位无符号的整型相当于Java语言的long。那么一个数值怎么表示多个状态呢其实是把long转换成一个64位的bit数组然后用每个bit数组中的0、1来表示对应的状态。Mark Word的头两位用来表示当前对象的状态两位只能表示4种状态00、01、10、11。在Mark Word中用01来表示无锁或者偏向锁的两种状态00表示轻量级锁10表示重量级锁11表示垃圾回收时的引用标志。MarkWord的整个状态表示如图所示。各个锁状态的详细信息如表所示。1.2、hashCode验证下面代码是简单的示例用来验证Mark Word在无锁状态下的hashCode值。在该示例中首先打印对象的Mark Word值然后调用hashCode方法来设置Hash值最后再次打印对象的Mark Word值。代码执行的结果如表所示。在调用hashCode方法后Mark Word的值是1945571841对应的二进制是11100111 1110111 0001001000000001Mark Word的最后两位是01表示处于无锁的状态后面都是hashCode的值。1.3、轻量级锁的状态信息下面代码是简单的轻量级锁的示例用来验证MarkWord在轻量级锁状态下的值。代码执行的结果如表所示。在获取轻量级锁之后Mark Word值是205810064对应的二进制数是110001000 1000110100110010000 Mark Word的最后两位从最初的01变成了00后面的数字都是当前线程的指针地址。1.4、重量级锁的状态信息如下代码是简单的重量级锁的示例用来验证MarkWord在重量级锁状态下的值。代码模拟了多个线程利用synchronized关键字同步加锁的情况。从上述代码中可以看到主线程处于等待状态详细信息如表所示。在获取轻量级锁之后Mark Word的值是71690680二进制是100010001011110 100110111000Mark Word的最后两位从最初的01变成了00。在获取重量级锁之后Mark Word的值是-645868214二进制是1111111111111111111111111111111111011001100000001101010101001010Mark Word的最后两位从最初的01变成了10。2、synchronized设计原理synchronized关键字是Java的内置同步锁实现了多线程的同步访问。synchronized可以用在方法或者代码块上。它确保在同一时刻只有一个线程可以执行被synchronized保护的方法或代码块。synchronized有3个特性原子性、可见性、有序性。原子性原子性就是指一个操作或者多个操作要么全部被执行并且执行过程不会被打断要么就都不会被执行。被synchronized修饰的方法或者代码块都是原子的因为在执行操作之前必须先获得类或对象的锁直到执行完才能释放对象锁。通过锁的机制实现了多线程操作的原子性。可见性可见性是指当多个线程同时访问一个数据时其中一个线程对数据的修改对其他线程实时可见。在任何一个时刻只有一个线程能获得同步的(synchronized)对象锁而锁的状态对其他任何线程都是实时可见的并且在释放锁之前会将当前线程对变量的修改同步到主内存中保证数据修改的多线程可见性。有序性synchronized具备有序性Java允许编译器和处理器对指令进行重排但是指令重排并不会影响单线程的逻辑顺序它影响的是多线程并发执行的顺序性。synchronized可以确保任意一个时刻只有一个线程可以访问同步代码块这就确保了代码执行的有序性。2.1、synchronized的使用synchronized关键字有两种用法一种是用于同步方法另一种是用于同步代码块同步方法又分为两种情况一种是用synchronized同步普通方法另一种是用synchronized同步静态方法。同步代码块也分为两种情况一种是用synchronized修饰的是对象实例的锁另一种是用synchronized修饰的是类对象的锁。synchronized的具体使用场景如图所示。1. 同步方法的实现机制如下代码是一个简单示例用来演示synchronized在方法上的并发控制即用synchronized修饰synMethod方法。在完成代码编译后我们可以通过javap- c xxx.class文件来查看Java编译后的字节码文件。synMethod方法的字节码内容如下方代码所示synMethod方法的flags字段上增加了ACC_SYNCHRONIZED并发控制标志。JVM在执行synMethod方法的时候首先会判断是否有ACC_SYNCHRONIZED标志如果有并发控制标志则JVM会先调用方法获取锁对象。2. 同步代码块的实现机制下方代码是一个简单示例用来演示synchronized在代码块上的使用。通过javap- c xxx.class文件我们可以清晰地看到编译后的字节码内容。下方代码是synCode方法的字节码synchronized的关键字编译成了monitorenter指令和monitorexit的指令。JVM就是通过这两条指令建立一段串行代码的执行区域在同一时刻只有一个线程能执行这个区域的代码。2.2、synchronized的具体设计为了减少synchronized获得锁和释放锁带来的相关性能消耗JDK 1.6引入了“偏向锁”和“轻量级锁”的概念。synchronized内置锁一共有4种状态级别从低到高依次是无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态这几种状态会随着竞争情况逐渐升级如图所示。锁可以升级但不能降级目的是提高获得锁和释放锁的效率。无锁状态表示对象是空闲状态不存在线程对对象的竞争。偏向锁主要用来优化同一线程多次申请同一个锁的情况。在很多场景中大部分时间一个锁都是被一个线程所持有。偏向锁的加锁整个过程只用修改对象的Mark Word值重点是将偏向锁的标志0改成1并将当前的线程ID设置到Mark Word中的线程ID字段原来Mark Word中的年龄等字段保持不变。具体修改过程是基于已有的Mark Word年龄、偏向锁状态、线程ID等参数构造一个新的Mark Word然后采用CAS机制整体替换掉老的。偏向锁的获取流程如图所示。如果JVM没有开启偏向锁或偏向锁获取失败会直接升级到轻量级锁的获取。轻量级锁的获取流程如图所示具体处理流程如下。首先判断锁对象的Mark Word是否空闲如果不空闲直接获取重量级锁。如果对象的Mark Word是空闲的将Mark Word复制备份。将对象的Mark Word的头两位设置成00并将线程栈中锁记录的指针复制到Mark Word的后62位中。通过CAS的方式更新Mark Word如果CAS修改失败线程将会进入重量级锁的竞争。当在轻量级锁获取失败之后就会升级到重量级锁的竞争。重量级锁整体是通过Object-Monitor来实现的。重量级锁的获取流程如图所示。synchronized重量级锁的获取流程如下。尝试快速获取监视器锁如果成功获取到锁将ObjectMonitor的owner字段指向当前线程。如果获取监视器锁失败将当前线程加入cxq队列中进行等待。其他线程释放锁之后会尝试唤醒当前线程继续来获取锁。3、synchronized源码分析在代码编译的时候方法上的synchronized关键字会被编译成ACC_SYNCHRONIZED标志代码块上的synchronized关键字会被编译成monitorenter指令和monitorexit指令。3.1、ACC_SYNCHRONIZED解析过程bytecodeInterpreter是JVM执行引擎的编译器主要用于解释并执行字节码。当发现方法上有ACC_SYNCHRONIZED标志bytecodeInterpreter会获取监视器锁。在获取到监视器锁之后才会执行方法。ACC_SYNCHRONIZED标志的解析流程如图所示。ACC_SYNCHRONIZED标志的具体解析过程如代码所示。3.2、monitorenter指令解析过程bytecodeInterpreter会将monitorenter指令解析成获取对象锁的执行逻辑流程如图所示。monitorenter指令的执行过程如代码所示。monitorenter指令的执行过程如下。从栈的底部寻找空闲的偏向锁指针将偏向锁指向锁对象。尝试获取偏向锁如果获取成功了执行代码。如果偏向锁获取失败了则调用InterpreterRuntime的monitorenter方法通过排队机制获取对象锁。3.3、monitorexit指令解析过程bytecodeInterpreter会将monitorexit指令翻译成锁释放的执行逻辑。monitorexit指令的解释过程如代码所示。3.4、锁获取实现过程由前可知不论是ACC_SYNCHRONIZED方法标志还是_monitorenter指令最终都是通过InterpreterRuntime类的monitorenter方法来获取锁的。InterpreterRuntime类的monitorenter方法逻辑也非常简单就是调用ObjectSynchronizer类的enter方法来获取对象锁具体实现如代码所示。1. enter方法enter方法是获取监视器锁的入口方法。enter方法的处理流程如下。判断是否启用了偏向锁如果启用了偏向锁则尝试获取偏向锁如果没有启用偏向锁则获取轻量级锁。判断对象是否处于无锁状态如果处于无锁状态则尝试获取轻量级锁。如果对象已经有锁了则判断锁是否由当前线程持有如果是由当前线程持有则属于锁重入的情况直接返回。如果无法获取轻量级锁则调用inflate方法获取ObjectMonitor对象然后调用ObjectMonitor的enter方法来获取重量级锁。2. inflate方法inflate方法的功能是获取ObjectMonitor对象其实现细节如代码所示。多线程同时获取对象监视器ObjectMonitor可能会遇到如下几种情况。如果其他线程已经获取到ObjectMonitor则当前线程就直接返回其他线程构造的ObjectMonitor对象。如果其他线程正在获取ObjectMonitor对象的过程中则当前线程要进行避让以免发生并发冲突。如果其他线程获取了轻量级锁则当前线程会构建一个新ObjectMonitor对象并把ObjectMonitor对象的持有线程设置成其他线程。如果其他线程已经释放了对象锁那么当前线程构造一个新的ObjectMonitor对象。3.5、锁释放实现过程InterpreterRuntime的monitorexit方法的功能是完成锁的释放。从下面代码中可以清晰地看到monitorexit方法是通过ObjectSynchronizer的exit方法来实现锁释放逻辑的。ObjectSynchronizer的exit方法首先会判断锁是否已经释放了如果已经释放了就直接返回。如果锁没有释放exit方法会调用ObjectMonitor的exit方法来释放监视器锁。exit方法的具体实现如代码所示。下面进行简单总结。在编译阶段javac编译器会将synchronized关键字编译为ACC_SYNCHRONIZED标记、monitorenter指令、monitorexit指令。在代码执行阶段JVM会把ACC_SYNCHRONIZED标志与monitorenter指令解析成获取对象锁的逻辑并调用InterpreterRuntime的monitorenter方法来获取对象锁。锁获取的过程如图所示。在代码执行结束的时候JVM会把ACC_SYNCHRONIZED标记与monitorexit指令解析成释放锁的逻辑并调用InterpreterRuntime的monitorexit方法来释放锁。锁释放的过程如图所示。4、volatile实现原理volatile是一种轻量级线程同步机制它只保证了可见性与有序性但无法保证原子性。相比synchronizedvolatile有一些自身的优势使用比较简单并且运行的效率也比synchronized更高。每个Java线程都拥有一个独立的工作内存同时有个全局内存堆内存来存储数据。当线程需要访问一个变量时首先将其复制到线程的工作内存中。之后线程每次对该变量的操作都将是对线程栈中的副本进行操作的。如果变量是被volatile修饰的每次变量都会直接从内存中读取数据每次对变量修改都会实时同步到内存中这样就能确保变量的多线程实时可见。volatile对任意变量的读写都具备原子性但对复合操作不具备原子性。所有基础类型与引用类型的赋值都是原子性的但JVM会将i这类复合操作解析成多条指令来执行所以不是原子操作。4.1、实现原理概述如下代码是一个简单的例子用来演示volatile的功能。VolatileTest类里定义了一个volatile的size字段然后写了一个简单的方法让size进行了加1操作。Java文件在编译之后会生成字节码文件可以通过javap来查看字节码文件并通过javap-v-p VolatileTest.class命令解析出字节码文件的内容详细信息如代码所示。字节码内容包含常量池、字段定义、方法定义、方法内容等信息。size字段有ACC_PRIVATE、ACC_VOLATILE访问标志。ACC_PRIVATE表示这个字段是私有的ACC_VOLATILE表示这个字段是由volatile关键字修饰的。数据的读取是通过getfield命令来实现的数据的赋值是通过putfield命令来实现的。4.2、getfield指令实现过程在执行getfield指令读取变量时JVM会先判断变量是否有ACC_VOLATILE标志。如果有ACC_VOLATILE标志则JVM会强制使CPU本地缓存失效从内存中直接读取数据。如果变量没有ACC_VOLATILE标志则JVM会从CPU本地缓存中读取数据。getfield指令解析过程如代码所示。以int类型的变量为例如果变量有ACC_VOLATILE标志JVM会调用int_field_acquire方法来读取数据如果变量没有ACC_VOLATILE标志JVM会调用int_field方法来获取数据。int_field_acquire方法会调用Atomic类的load_acquire方法来读取数据。load_acquire方法会调用OrderAccess的acquire方法来使CPU的本地缓存失效。load_acquire方法的实现如代码所示。OrderAccess的acquire方法会根据不同CPU型号来发送不同的指令信息。下方代码是x86处理内存屏障的实现。compiler_barrier函数是直接面向CPU硬件编程的是采用内嵌汇编命令来实现的。asm指令表示当前代码是汇编代码。volatile指令用来禁止编译器对代码进行优化。memory指令用来让CPU本地缓存失效。4.3、putfield指令实现过程在执行putfield指令修改变量时JVM会先判断变量是否有ACC_VOLATILE标志。如果变量有ACC_VOLATILE标志在修改完数据后JVM会调用OrderAccess的storeload方法来刷新CPU缓存将CPU缓存中的数据同步到主内存中。如果变量没有ACC_VOLATILE标志JVM会直接将数据写入CPU缓存。putfield指令的解析过程如代码所示。以int类型的变量为例如果变量有ACC_VOLATILE标志JVM会调用release_int_field_put方法来写入数据。release_int_field_put方法最终是调用Atomic类的release_store方法来实现数据写入。在写入数据之前release_store方法会调用OrderAccess的release方法使CPU本地缓存失效然后写入数据。volatile数据写入的具体实现如代码所示。OrderAccess的release方法在不同CPU有不同的实现的方式。release方法和acquire方法实现逻辑相同都是调用了compiler_barrier来实现内存屏障使当前CPU本地缓存失效。而storeload则是先对内存地址加锁再加上内存屏障来实现内存同步。5、volatile伪共享volatile关键字是通过CPU缓存与内存数据的实时同步来实现多线程的可见性的。每次内存与CPU缓存之间的数据同步是以缓存行(Cache Line)为单位的。缓存行是CPU的最小缓存单位大小为64字节逻辑结构如图所示。CPU每次从内存往CPU缓存读取数据或者从CPU缓存向内存同步数据都是以一个缓存行作为单位的。这样做是为了提升CPU缓存与内存之间的数据交换效率。缓存行虽然提高了数据传输效率但也带来了新的问题。变量a与变量b在同一个缓存行中。CPU0上的线程用到了变量aCPU1的线程用到了变量b。变量a是用volatile修饰的那每次线程对变量a的修改都会让CPU0的缓存行失效并将消息广播到CPU1。CPU1收到缓存广播失效了以后就会为CPU缓存中的b打上失效标记。当线程2需要读取b的时候会直接从缓存中读取数据。如果同一个缓存中存储了多个变量并且变量都是用volatile修饰的多个线程同时对缓存行中的多个变量进行修改就会产生大量的CPU缓存数据失效的消息这将极大地降低CPU的运行效率。缓存行失效的示意图如图所示。那如何解决这个问题呢可以在volatile修饰的字段后面填充无效的数据使得无效数据刚好填满一个缓存行也就是我们常说的volatile伪共享。在JDK8以前只能通过手动方式填充无效字段。代码清单5-25是一个简单的计数器用来演示如何手动填充数据。计数器Counter内部定义了一个long型的字段count。因为Java里面long只占用8个字节而要确保long型字段能够在一个缓存行里面则需要填充56个字节的无效数据所以代码里定义了7个无效的long型字段即p1p7。在JDK8以后Java提供了sun.misc.Contended注解来实现自动填充但同时需要设置JVM的启动参数-XX:-RestrictContended。可以把上面的例子进行简单改造在count字段上加上Contended注解即可改造后的代码如代码所示。那手动填充字段和Contended注解有什么差别呢手动填充需要在编码时计算出要填充多少个数据字段而如果采用自动填充方式开发人员则不用关心此类问题。6、CAS硬件同步原语6.1、CAS硬件原语CAS是解决多线程并行情况下使用锁造成性能损耗的一种机制采用这种无锁的原子操作可以实现线程安全避免加锁带来的笨重性。CAS操作包含3个操作参数内存位置(V)、预期原值(A)、新值(B)。伪代码如下所示如果内存位置的值与预期原值相等那么CPU自动将该位置值更新为新值。如果内存位置的值与预期原值不相等则处理器不进行任何操作。CAS操作是通过CPU指令来完成的它需要硬件的支持。6.2、JVM CAS实现在JDK1.5以后Java就提供了CAS机制来实现线程安全的控制。具体来说sun. misc.Unsafe类里的compareAndSwapInt和compareAndSwapLong方法提供了CAS的功能JVM里面的Atomic类的cmpxchg方法实现了CAS功能。Atomic是个抽象类不同的操作系统与处理器上有具体的实现如下代码是Linux系统x86处理器上的具体实现。CPU将exchange_value加载到rax寄存器中rax寄存器用来存储最终返回结果的值。CPU将compare_value的值存入eax寄存器。dest表示数据对象的当前值该值可存入任意的通用寄存器。比较eax寄存器的值compare_value和dest寄存器的值是否相等。如果相等则把exchange_value的值写入dest寄存器完成新值的设置。如果不相等则把dest寄存器中的值写入eax寄存器。返回eax寄存器中的exchange_value值。如果exchange_value等于compare_value表示这次修改失败了。如果exchange_value等于要修改的值表示修改成功了。6.3、ABA问题但是CAS会有一个ABA的问题如图所示。变量A在内存中最初的值为10有3个线程都需要对变量A进行操作。最初线程1读取了变量A的值在线程1读取后线程2把A的值改成20然后线程3又把变量A的值改成10。最后线程1采用CAS的方式想把A改成30这时由于A的预期值为10A的当前值也是10此时线程1就会错误地把A改成30。原来CAS的预期是从T1时刻读取数据到T4时刻去修改数据。这中间A是没有变化的但实际情况是A经历了10→20→10的变化。这里就对CAS的使用提出了一个要求要求在一定的时间周期内数据的变化是不可逆的是单向线性变化的我们需要规避ABA这种可逆性的改变。CAS的核心思想是把变量的当前值和预期值进行比较如果当前值等于预期值就会把变量设置成一个新的值。如果当前值不等于预期值说明变量已经发生了变化就不进行修改。在使用CAS进行数据修改的时候一定要考虑ABA问题要确保在一定周期内数据的变化是不可逆的。7、Unsafe功能介绍Unsafe类在sun.misc包路径下是由sun公司实现的扩展工具类主要提供一些直接面向JVM内部操作的功能。由于Unsafe可直接操作JVM因此操作不当会导致程序的整体性崩溃。Unsafe的含义是提醒大家要注意使用时的安全确保不会导致程序崩溃。Unsafe提供的功能如图所示。7.1、操作内存Unsafe提供了直接操作JVM内存的能力主要包含内存的分配、复制、释放、给定地址值操作等方法如代码所示。其中allocateMemory方法的功能是申请堆外内存通过allocateMemory方法申请的内存需要手动释放垃圾收集器不会进行垃圾回收。DirectByteBuffer就是通过这个方法来申请本地内存的。reallocateMemory方法的功能是对已经申请的内存进行缩容与扩容。freeMemory方法是用来释放前面申请的内存。setMemory方法用于设置内存空间的值例如DirectByteBuffer在申请完内存空间后会调用setMemory来设置默认值。7.2、获取字段内存偏移量Unsafe提供了获取JVM对象字段内存位置的能力主要包含获取普通字段、static字段、数组等的内存偏移量如代码所示。Java对象在JVM中对象是用OOP表示的OOP包含Mark Word、Class指针、属性信息等内容。objectFieldOffset方法就是用来获取具体属性的偏移量。通过对象的首地址加上偏移量JVM就能准确地获取到内存的绝对地址。JVM就可以通过绝对地址去进行赋值。通过上面这种方式赋值减少了中间换算的过程极大提升了访问效率。7.3、普通字段的读取与赋值Unsafe提供了直接读写JVM对象属性的能力包含对常见的8种基础数据类型的读取与赋值的能力方法描述如代码所示。7.4、volatile字段的读取与赋值Unsafe提供了volatile字段的读取与赋值能力包含对常见的8种基础数据类型的读取与赋值方法描述如代码所示。7.5、CAS操作能力Unsafe提供了对int、long、对象引用三种数据类型的CAS操作能力如代码所示。ConcurrentHashMap、AtomicBoolean、AtomicInteger、AbstractQueuedLongSynchronizer等类都使用了compareAndSwapInt方法来实现对应的业务逻辑。AtomicLong、AtomicLo-ngArray等类都使用compareAndSwapLong来实现业务功能。AtomicReference、Atomic-MarkableReference等类都使用compareAndSwapObject来实现安全修改对象的属性引用。7.6、线程阻塞与唤醒Unsafe提供了线程的阻塞与唤醒能力如代码所示。7.7、内存屏障Unsafe提供了3种内存屏障的能力。loadFence读屏障的功能是让CPU本地缓存失效。storeFence写屏障的功能是将CPU本地缓存中修改的数据及时同步到主存中。fullFence屏障的作用相当于storeFence加loadFence执行过程是先触发CPU本地缓存进行数据同步再使CPU本地缓存中的数据失效。Unsafe方法的内存屏障功能如下所示。StampedLock就是通过调用loadFence来实现实时读取内存中的数据的。MethodHandle就是通过调用fullFence来实现Lambda表达式的更新与优化的。8、Unsafe实现原理8.1、volatile字段读取在JVM里volatile字段的读取功能是通过Unsafe_Get函数实现的JNI描述如下所示。Unsafe_Get函数实际是调用MemoryAccess的get_volatile函数来进行数据读取的。如下代码是get_volatile函数的具体实现。RawAccessBarrier是RawAccess的实现类它通过load_internal函数完成数据读取。load_internal是通过调用Atomic类的load_acquire函数来完成数据读取具体实现可参考如下代码。load_acquire功能就是在读取数据前加入内存屏障让CPU的缓存失效然后从主内存读取数据。![!](https://i-blog.csdnimg.cn/direct/876af8ec2ec74324b17bbf9b1234e871.png)8.2、volatile字段写入volatile字段的赋值能力主要是通过Unsafe_Put函数实现的。Unsafe_Put函数的描述如代码所示。Unsafe_Put函数是通过MemoryAccess类的put_volatile函数来实现的。put_volatile函数具体实现如代码所示。put_volatile函数会调用RawAccess的store函数来完成写入。store函数对应的具体实现是store_internal函数。如下代码是store_internal函数的具体代码实现先将数据写入CPU缓存中然后通过写屏障将CPU本地的缓存数据同步到主内存中最后调用内存读屏障来使CPU的本地缓存失效。8.3、CAS操作能力compareAndSwapInt方法对应的JNI是Unsafe_CompareAndSetInt函数compareAnd-SwapLong方法对应的JNI是Unsafe_CompareAndSetLong函数compareAndSwapObject方法对应的JNI是Unsafe_CompareAndSetReference函数CAS JNI函数如代码所示。如下代码是Unsafe_CompareAndSetInt函数的代码核心逻辑是先获取到obj的内存地址接着根据对象的内存地址加上内存偏移量(offset)来获取字段的内存地址。然后调用Atomic类的cmpxchg方法来实现数据的赋值cmpxchg方法先会比较addr内存中的值有没有改变没有改变就赋予新的值。如下代码是Unsafe_CompareAndSetReference函数的代码核心逻辑也是获取到对象的内存地址然后调用HeapAccess的oop_atomic_cmpxchg_at方法来实现数据的比较与交换。8.4、线程阻塞与唤醒Unsafe分别通过park和unpark方法提供了线程的阻塞与唤醒能力二者在JVM里对应处理的函数是Unsafe_Park与Unsafe_Unpark函数如代码所示。Unsafe_Park函数会先获取当前线程的Parker对象然后调用Parker对象的park方法来阻塞线程。Unsafe_Unpark函数会先获取当前线程的Parker对象然后调用Parker的unpark方法来唤醒线程。9、LockSupport实现原理LockSupport方法是通过sun.misc.Unsafe来实现线程阻塞与唤醒的。LockSupport方法提供了一组线程阻塞与唤醒的方法详细方法如表所示。LockSupport提供了2种线程阻塞的方式一种是不带阻塞对象的方法另一种是带阻塞对象的方法。阻塞对象可以表示线程阻塞的原因JVM会把阻塞对象设置到线程的parkBlocker字段中这样我们就可以通过诊断工具查看线程阻塞的原因。9.1、Unsafe初始化在使用Unsafe之前需要先实例化Unsafe。如代码所示LockSupport定义了UNSAFE静态全局变量。9.2、无阻塞对象方法LockSupport提供了3个无阻塞对象的线程阻塞方法如代码所示。这3个方法最终都是调用Unsafe的park方法来实现线程阻塞的。9.3、有阻塞对象方法LockSupport提供了3种有阻塞对象的线程阻塞方法。这3个方法的处理流程基本都是一样的首先调用setBlocker方法设置阻塞对象然后调用Unsafe的park方法来阻塞当前线程线程醒来后会再次调用setBlocker方法清除绑定对象如代码所示。在上述代码中setBlocker方法通过调用Unsafe的putObject方法将阻塞对象设置到当前线程的parkBlocker字段中。如下代码展示了有阻塞对象与无阻塞对象之间的差别。LockSupportTest有2个方法park方法与parkObject方法。调用park方法来实现线程阻塞时可通过Arthas的thread命令来查看线程的信息线程阻塞结果如图所示。从上图中可以清晰地看到当前线程被sun.misc.Unsafe.park方法阻塞了线程处于WAITING状态但不知道线程是因为什么而阻塞的。调用parkObject方法来实现线程阻塞时可通过Arthas的thread命令来查看线程的信息线程对象阻塞结果如图所示。从上图中可以清晰地看到线程被sun.misc.Unsafe.park方法阻塞了并阻塞在java.lang.Object对象上这样就能清晰地知道线程是因为什么而阻塞了。9.4、线程唤醒Unsafe的unpark方法用来唤醒线程下面代码是unpark方法的具体实现。