什么是JMM可见性?
可见性是JMM三大特性之一,指的是当一个线程修改了共享变量的值,其他线程都可以看到修改后的值。
如何保证可见性?
/**
* @author sonnie guo
* @version 1.0
* @className VisibilityTest
* @description
* @date 2022/2/20 22:21
*/
public class VisibilityTest {
private boolean flag = true;
private int count = 0;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag:" + flag);
}
public void load() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
while (flag) {
//TODO 业务逻辑
count++;
}
System.out.println(Thread.currentThread().getName() + "跳出循环: count=" + count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest test = new VisibilityTest();
// 线程threadA模拟数据加载场景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 让threadA执行一会儿
Thread.sleep(1000);
// 线程threadB通过flag控制threadA的执行时间
Thread threadB = new Thread(() -> test.refresh(), "threadB");
threadB.start();
}
}
很明显,对于上述代码,我们可以有很多方法来保证可见性,从而使线程A跳出循环。例如:
-
延长do-while时长
shortWait(10000000); public static void shortWait(long interval) { long start = System.nanoTime(); long end; do { end = System.nanoTime(); } while (start + interval >= end); }
-
调用System.out.println()输出
System.out.println(count);
-
线程休眠
try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
-
线程让步
Thread.yield();
-
将flag设置为volatile属性
private volatile boolean flag = true;
-
将count设置为Integer属性
private Integer count = 0;
-
利用内存屏障:
UnsafeFactory.getUnsafe().storeFence();
-
LockSupport.unpark(Thread.currentThread());
-
锁机制
Lock lock = new ReentrantLock(); lock.lock(); lock.unlock();
我们一个一个来看。
-
延长do-while时长:缓存过期,重新从主内存加载
-
线程让步:释放时间片,上下文切换,再次拿到时间片之后,会去“还原现场”,也就是重新加载上下文
-
volatile:
-
JMM内存交互层面
volatile修饰的变量,其read,load,use,assign,store,write必须是连续的,也就是修改后必须立即同步回主内存,使用时必须立即从主内存刷新,由此保证volatile对变量操作在多线程环境中的可见性。
-
硬件层面
通过Lock前缀指令,锁定变量缓存行区域,并写回到主内存,也就是"缓存锁定",缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。
-
volatile在hotspot的实现
(字节码解释器实现)
可以看到,程序会先判断这个变量又没有没volatile修饰,有的话则调用OrderAccess:😗*storeload()**方法。
storeload,也就是JVM层面的内存屏障(要与处理器的内存屏障区分开哦~),storeload方法内部调用了fence方法,fence方法先判断处理器是否为多核,如果为多核则调用==lock; addl $0,0(%%rsp)==指令。
这个lock前缀指令并非内存屏障指令,由于它可以起到内存屏障的效果,还可以使缓存失效,并且其性能也要优于mfence内存屏障,所以选用它。那么它究竟有哪些作用呢?
- 确保后续指令执行的原子性。在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低 lock前缀指令的执行开销。
- LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排序。
- LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新 store buffer的操作会导致其他cache中的副本失效。
(模板解释器的实现)
-
-
利用内存屏障:UnsafeFactory.getUnsafe().storeFence();
实现原理和volatile相同,底层都是依靠lock前缀指令来实现可见性的。
-
调用System.out.println()输出:
其实现方法如下,可见她是依靠synchronized来实现可见性的。不过,synchronized底层也用到了内存屏障。
public void println(int x) { synchronized (this) { print(x); newLine(); } }
-
LockSupport.unpark(Thread.currentThread());
底层也是依靠内存屏障来实现。
-
线程休眠:实际上底层用到了内存屏障,
-
Integer
Integer在拆包时会new Integer,其中value属性是由final修饰的。
final关键字也是会保证可见性的。
/** * The value of the {@code Integer}. * * @serial */ private final int value; public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } public Integer(int value) { this.value = value; }
总结
因此,我们可以知道,Java中可见性如何保证?
方式归类有两种:
- jvm层面:storeLoad内存屏障 ===> x86 lock替代了mfence
- 上下文切换 例如:Thread.yield();