企业网站的建设 英文摘要,苏州建网站公司选苏州聚尚网络,大连网络营销公司哪家好,安徽网站建设维护目录
编辑
一、可见性概念
1.1 概念
二、可见性问题由来
2.1 由来分析
三、可见性代码例子
3.1 代码
3.2 执行结果
四、Java 中保证可见性的手段
4.1 volatile
4.1.1 优化代码
4.1.2 测试结果
4.1.3 volatile原理分析
4.1.3.1 查看字节码
4.1.3.2 hotspot 层面…
目录
编辑
一、可见性概念
1.1 概念
二、可见性问题由来
2.1 由来分析
三、可见性代码例子
3.1 代码
3.2 执行结果
四、Java 中保证可见性的手段
4.1 volatile
4.1.1 优化代码
4.1.2 测试结果
4.1.3 volatile原理分析
4.1.3.1 查看字节码
4.1.3.2 hotspot 层面
4.1.3.3 volatile原理总结
4.2 synchronized
4.2.1 代码优化
4.2.2 测试结果
4.2.3 synchronized 原理分析
4.2.3.1 synchronized 修饰方法
4.2.3.1.1 源代码
4.2.3.1.2 执行结果
4.2.3.1.3 编译分析
4.2.3.2 synchronized 修饰代码块
4.2.3.2.1 源代码
4.2.3.2.2 执行结果
4.2.3.2.3 编译分析
4.3 Lock
4.3.1 优化代码
4.3.2 测试结果
4.3.3 Lock实现可见性原理分析
4.3.3.1 源码分析
4.3.3.2 总结
4.4 final
4.4.1 final 实现可见性原理分析 一、可见性概念
1.1 概念
可见性是指当一个线程修改了共享变量后其他线程能够立即得知这个修改。
二、可见性问题由来
2.1 由来分析
可见性问题是在CPU位置出现的CPU处理速度非常快相对CPU来说去主内存获取数据这个事情太慢了为了解决CPU加载主内存数据慢的问题就在CPU加入了缓存寄存器分别为L1、L2、L3三级缓存每次去主内存拿完数据后就会存储到CPU的三级缓存每次去三级缓存拿数据效率肯定会提升。如下图所示 引入这种缓存机制后这就带来了问题现在CPU都是多核每个线程的工作内存CPU三级缓存都是独立的会告知每个线程中做修改时只改自己的工作内存没有及时的同步到主内存导致主内存和缓存之间数据不一致问题。而数据不一致的问题换据说法就是可见性问题。
为了解决CPU硬件层面的缓存一致性问题于是就设计出了缓存一致性协议其中比较典型的就是MESI协议但是这个协议其实不同的CPU厂商的实现方式是有差异的Java 层面为了屏蔽各种硬件和操作系统带来的差异让并发编程做到真正意义上的跨平台就设计出了JMM即Java Memery Model Java 内存模型来解决。
三、可见性代码例子
3.1 代码
package com.ningzhaosheng.thread.concurrency.features.visible;/*** author ningzhaosheng* date 2024/2/5 19:36:39* description 测试可见性*/
public class TestVisible {private static boolean flag true;public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {while (flag) {// ....}System.out.println(t1线程结束);});t1.start();Thread.sleep(10);flag false;System.out.println(主线程将flag改为false);}
}
3.2 执行结果 由结果可知,主线程修改了flag false;但是并没有使t1线程里面的循环结束。
四、Java 中保证可见性的手段
4.1 volatile
4.1.1 优化代码
package com.ningzhaosheng.thread.concurrency.features.visible.volatiles;/*** author ningzhaosheng* date 2024/2/5 19:45:29* description 测试volatile*/
public class TestVolatile {private volatile static boolean flag true;public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {while (flag) {// ....}System.out.println(t1线程结束);});t1.start();Thread.sleep(10);flag false;System.out.println(主线程将flag改为false);}
}
4.1.2 测试结果 由以上测试结果可以看到使用volatile 修饰共享变量之后在主线程修改了flag false 之后线程t1读取到了最新值并结束了循环结束了线程。那么为什么使用了volatile之后就能解决共享变量的问题呢要回答这个问题其实综合性考虑的内容还比较多涉及到CPU的多级缓存、计算机缓存一致性协议和Java 内存模型等相关内容。我们接下来就分析下吧。
4.1.3 volatile原理分析
4.1.3.1 查看字节码
javap -v .\TestVolatile.class 从以上截图我们可以看到使用volatile修饰的变量会多一个ACC_VOLATILE 指令关键字。我们接着去hotspot 查看c源码分析ACC_VOLATILE做了些什么操作。
4.1.3.2 hotspot 层面
根据ACC_VOLATILE指令关键字我们可以在hotspot 源码中找到他的内容
jdk8u/jdk8u/hotspot: 69087d08d473 src/share/vm/utilities/accessFlags.hpp (openjdk.org) 接着我们找下is_volatile
jdk8u/jdk8u/hotspot: 69087d08d473 src/share/vm/interpreter/bytecodeInterpreter.cpp (openjdk.org) 从以上截图的这段代码中可以看到会先判断tos_type(volatile变量类型)后面有不同的基础类型的调用比如int类型就调用release_int_field_putbyte就调用release_byte_field_put等等。 判断完类型之后我们可以看到代码后面执行的语句是 我们可以在以下代码位置找到该源码
jdk8u/jdk8u/hotspot: 69087d08d473 src/share/vm/runtime/orderAccess.hpp (openjdk.org) 实际上storeload() 这个方法针对不同CPU有不同的实现它的具体实现在src/os_cpu下我们可以去看一下 这里我们以linux_x86架构的CPU实现为例我们去看下storeload()方法做了些什么操作。
jdk8u/jdk8u/hotspot: 69087d08d473 src/os_cpu/linux_x86/vm/orderAccess_linux_x86.inline.hpp (openjdk.org) 接着看下fence()函数 通过这面代码可以看到lock;add1其实这个就是内存屏障。lock;add1 $0,0(%%esp)作为cpu的一个内存屏障。 add1 $0,0(%%rsp)表示将数值0加到rsp寄存器中而该寄存器指向栈顶的内存单元。加上一个0rsp寄存器的数值依然不变。即这是一条无用的汇编指令。在此利用add1指令来配合lock指令用作cpu的内存屏障。
内存屏障
这四个分别对应了经常在书中看到的JSR规范中的读写屏障LoadLoad屏障指令Load1; LoadLoad; Load2在Load2及后续读取操作要读取的数据被访问前保证Load1要读取的数据被读取完毕。
LoadStore屏障指令Load1; LoadStore; Store2在Store2及后续写入操作被刷出前保证Load1要读取的数据被读取完毕。StoreStore屏障指令Store1; StoreStore; Store2在Store2及后续写入操作执行前保证Store1的写入操作对其它处理器可见。StoreLoad屏障指令Store1; StoreLoad; Load2在Load2及后续所有读取操作执行前保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中这个屏障是个万能屏障兼具其它三种内存屏障的功能
对于volatile操作而言其操作步骤如下
每个volatile写入之前插入一个 StoreStore 写入以后插入一个StoreLoadJMM会将当前线程对应的CPU缓存及时的刷新到主内存中。每个volatile读取之前插入一个 LoadLoad 读取之后插入一个LoadStoreJMM会将对应的CPU缓存中的内存设置为无效必须去主内存中重新读取共享变量。
4.1.3.3 volatile原理总结
通过编译的字节码分析我们可以知道使用volatile 修饰的变量编译后会生成ACC_VOLATILE关键字通过关键字搜索我们在hotspot 源码层JVM层查询到is_volatile函数这个函数的作用就是会先判断tos_type(volatile变量类型)后面有不同的基础类型的调用比如int类型就调用release_int_field_putbyte就调用release_byte_field_put等等还有就是调用了一个OrderAccess::storeload();函数最终我们通过查看源码找到storeload方法在不同CPU架构下的实现最终基本可以得出以下结论
在JVM层volitile的底层在JVM层其实是通过内存屏障防止了指令重排序。CPU层面在x86的架构中含有lock前缀的指令拥有两种方法实现一种是开销很大的总线锁它会把对应的总线直接全部锁住如此明显是不合理的所以后期intel引入了缓存锁以及mesi协议如此便可以轻量化的实现内存屏障
最终结论volatile的底层原理在JVM源码层次而言内存屏障直接起到了禁止指令重排的作用且之后与总线锁或者MESI协议配合实现了可见性即当写一个volatile变量JMM会将当前线程对应的CPU缓存及时的刷新到主内存中当读一个volatile变量JMM会将对应的CPU缓存中的内存设置为无效必须去主内存中重新读取共享变量。
4.2 synchronized
4.2.1 代码优化
package com.ningzhaosheng.thread.concurrency.features.visible.syn;/*** author ningzhaosheng* date 2024/2/5 19:52:31* description 测试synchronized*/
public class TestSynchronized {private static boolean flag true;public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {while (flag) {synchronized (TestSynchronized.class) {//...}System.out.println(111);}System.out.println(t1线程结束);});t1.start();Thread.sleep(10);flag false;System.out.println(主线程将flag改为false);}
}
4.2.2 测试结果 从测试结果可以看出使用了synchronized同步代码块之后在主线程中修改了flagfalse 之后线程t1也获取到最新的变量值结束了while循环。也就是说synchronized也可以解决并发编程的可见性问题。那么synchronized是怎么保证并发编程的可见性的呢我们接下来分析下。
4.2.3 synchronized 原理分析
4.2.3.1 synchronized 修饰方法
4.2.3.1.1 源代码
package com.ningzhaosheng.thread.concurrency.features.visible.syn;/*** author ningzhaosheng* date 2024/2/13 10:16:36* description synchronized 修饰方法*/
public class TestSynchronizedMethod {public static boolean flag true;public static synchronized void runwhile() {while (flag) {System.out.println(111);}System.out.println(t1线程结束);}public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {runwhile();});t1.start();Thread.sleep(10);flag false;System.out.println(主线程将flag改为false);}
}
4.2.3.1.2 执行结果 4.2.3.1.3 编译分析
javap -v .\TestSynchronizedMethod.class 可以看见使用synchronized修饰方法后通过javap -v 查看编译的字节码会生成一个ACC_SYNCHRONIZED标识符会隐式调用monitorenter和monitorexit。在执行同步方法前会调用monitorenter在执行完同步方法后会调用monitorexit。
可查看官网解析Chapter 2. The Structure of the Java Virtual Machine (oracle.com) 该标识符的作用是使当前线程优先获取Monitor对象同一个时刻只能有一个线程获取到在当前线程释放Monitor对象之前其它线程无法获取到同一个Monitor对象从而保证了同一时刻只能有一个线程进入到被synchornized修饰的方法。
获取到锁资源之后会将内部涉及到的变量从CPU缓存中移除且要求线程必须去主内存中重新拿数据在释放锁之后会立即将CPU缓存中的数据同步到主内存。
注意关于Monitor的更多底层实现原理由于篇幅原因这里先不分析后续会出相关文章详细说明synchronized这里只是就实现可见性原理做些说明。
4.2.3.2 synchronized 修饰代码块
4.2.3.2.1 源代码
package com.ningzhaosheng.thread.concurrency.features.visible.syn;/*** author ningzhaosheng* date 2024/2/13 10:48:02* description synchronized 修饰代码块*/
public class TestSynchronizedCodeBlock {public static boolean flag true;public static void runwhile() {while (flag) {synchronized (TestSynchronizedCodeBlock.class) {System.out.println(flag);}System.out.println(111);}System.out.println(t1线程结束);}public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {runwhile();});t1.start();Thread.sleep(10);flag false;System.out.println(主线程将flag改为false);}
}
4.2.3.2.2 执行结果 4.2.3.2.3 编译分析
javap -v .\TestSynchronizedCodeBlock.class 可以看到使用synchronized修饰代码块后查看编译的字节码会发现再存取操作静态共享变量时会插入monitorenter、monitorexit原语指令关于这两个指令的说明可查看文档
Chapter 6. The Java Virtual Machine Instruction Set (oracle.com) 它实现可见性的原理和上一小节说明的那样都是
当前线程优先获取Monitor对象同一个时刻只能有一个线程获取到在当前线程释放Monitor对象之前其它线程无法获取到同一个Monitor对象从而保证了同一时刻只能有一个线程进入到被synchornized修饰的代码块。
获取到锁资源之后会将内部涉及到的变量从CPU缓存中移除且要求线程必须去主内存中重新拿数据在释放锁之后会立即将CPU缓存中的数据同步到主内存。
4.3 Lock
4.3.1 优化代码
package com.ningzhaosheng.thread.concurrency.features.visible.lock;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** author ningzhaosheng* date 2024/2/5 19:57:24* description 测试Lock*/
public class TestLock {private static boolean flag true;private static Lock lock new ReentrantLock();public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {while (flag) {lock.lock();try {//...} finally {lock.unlock();}}System.out.println(t1线程结束);});t1.start();Thread.sleep(10);flag false;System.out.println(主线程将flag改为false);}
}
4.3.2 测试结果 4.3.3 Lock实现可见性原理分析
4.3.3.1 源码分析 通过以上截图可以看到我们创建了一个ReentrantLock调用了它的lock()方法而ReentrantLock实现了Lock接口和基于AQS定义实现了锁。
4.3.3.2 总结
Lock锁保证可见性的方式和synchronized完全不同synchronized基于他的内存语义在获取锁和释放锁时对CPU缓存做一个同步到主内存的操作。
Lock锁是基于volatile实现的。Lock锁内部再进行加锁和释放锁时会对一个由volatile修饰的state属性进行加减操作。
如果对volatile修饰的属性进行写操作CPU会执行带有lock前缀的指令CPU会将修改的数据从CPU缓存立即同步到主内存同时也会将其他的属性也立即同步到主内存中。还会将其他CPU缓存行中的这个数据设置为无效必须重新从主内存中拉取。
参考4.1.3.3 volatile原理总结部分。
4.4 final
4.4.1 final 实现可见性原理分析
final修饰的属性在运行期间是不允许修改的这样一来就间接的保证了可见性所有多线程读取final属性值肯定是一样。
final并不是说每次取数据从主内存读取他没有这个必要而且final和volatile是不允许同时修饰一个属性的。 final修饰的内容已经不允许再次被写了而volatile是保证每次读写数据去主内存读取并且volatile会影响一定的性能就不需要同时修饰。 好了本次内容就分享到这欢迎关注本博主。如果有帮助到大家欢迎大家点赞关注收藏有疑问也欢迎大家评论留言