栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

JMM模型一 解决可见性问题原理

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

JMM模型一 解决可见性问题原理

jmm模型是java多线程通信模型–java使用共享内存模型 jmm白话版就是在java中的线程跟线程是如何通信的一个模型 就是线程a如何得到线程b中的数据

public class VisibilityTest {
    //  storeLoad  JVM内存屏障  ---->  (汇编层面指令)  lock; addl $0,0(%%rsp)
    // lock前缀指令不是内存屏障的指令,但是有内存屏障的效果   缓存失效
    private volatile boolean flag = true;
    private Integer 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++;
        //JMM模型    内存模型: 线程间通信有关   共享内存模型
        //没有跳出循环   可见性的问题
        //能够跳出循环   内存屏障
        //UnsafeFactory.getUnsafe().storeFence();
        //能够跳出循环    ?   释放时间片,上下文切换   加载上下文:flag=true
        //Thread.yield();
        //能够跳出循环    内存屏障
        //System.out.println(count);

        //LockSupport.unpark(Thread.currentThread());

        //shortWait(1000000); //1ms
        //shortWait(1000);

//            try {
//                Thread.sleep(1);   //内存屏障
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
 

    }
    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();

}


public static void shortWait(long interval) {
    long start = System.nanoTime();
    long end;
    do {
        end = System.nanoTime();
    } while (start + interval >= end);
}
}

上面的代码的逻辑是threadA线程有一个死循环(while (flag) flag默认为true)不断的count++,threadB线程是对flag属性进行修改 修改为false 目的是让threadA的死循环停止
但是因为可见性问题需要做一些处理才能实现 首先说jmm模型

假设现在内存中有flag=true这个值

首先代码运行会把数据放到内存中  --threadA--》threadA得到flag=true然后开始死循环

这时1秒过后threadB启动 threadB也是会从内存中得到flag=true 然后对flag信息修改 修改为flag=false 然后写回内存  这时按着正常想法threadA的循环会结束
自行脑部一下图

但是他不会结束 每个线程都有一个本地内存 从内存中得到flag=true之后先写道本地内存 然后再从本地内存读取flag=true 然后执行逻辑 当线程threadB修改了flag之后写到
内存 因为线程会优先从本地内存获取数据 如果找不到才从内存中获取 threadA现在可以从本地内存中找到flag不会去内存中重新获取所以还是会一直循环 如果想让threadA结束
循环就需要让threadA的本地内存中的数据失效让它再次从内存中获取一遍数据

以下操作都可以让本地内存中的数据失效

通过 volatile 关键字保证可见性。
通过 内存屏障保证可见性。
通过 synchronized 关键字保证可见性。
通过 Lock保证可见性。
通过 final 关键字保证可见性


 //JMM模型    内存模型: 线程间通信有关   共享内存模型
            //没有跳出循环   可见性的问题
            //能够跳出循环   内存屏障
            //UnsafeFactory.getUnsafe().storeFence();
            //能够跳出循环    ?   释放时间片,上下文切换   加载上下文:flag=true
            //Thread.yield();
            //能够跳出循环    内存屏障
            //System.out.println(count);

        //LockSupport.unpark(Thread.currentThread());

        //shortWait(1000000); //1ms
        //shortWait(1000);

//            try {
//                Thread.sleep(1);   //内存屏障
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }

//总结: Java中可见性如何保证? 方式归类有两种:
//1. jvm层面 storeLoad内存屏障 ===> x86 lock替代了mfence
// 2. 上下文切换 Thread.yield();

首先内存屏障为什么会让本地内存中的数据失效

volatile在hotspot的实现
字节码解释器实现
JVM中的字节码解释器(bytecodeInterpreter),用C++实现了JVM指令,其优点是实现相对简单且容易理解,缺点是执行慢。
bytecodeInterpreter.cpp

判断是否加了volatile关键字

在linux系统x86中的实现
orderAccess_linux_x86.inline.hpp

inline void OrderAccess::storeload()  { fence(); }
inline void OrderAccess::fence() {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }

lock; addl $0,0(%%rsp)在这加了一个lock前缀指令

模板解释器实现
模板解释器(templateInterpreter),其对每个指令都写了一段对应的汇编代码,启动时将每个指令与对应汇编代码入口绑定,可以说是效率做到了极致。
templateTable_x86_64.cpp

void TemplateTable::volatile_barrier(Assembler::Membar_mask_bits
                                     order_constraint) {
  // Helper function to insert a is-volatile test and memory barrier
  if (os::is_MP()) { // Not needed on single CPU
    __ membar(order_constraint);
  }
}

// 负责执行putfield或putstatic指令
void TemplateTable::putfield_or_static(int byte_no, bool is_static, RewriteControl rc) {
	// ...
	 // Check for volatile store
    __ testl(rdx, rdx);
    __ jcc(Assembler::zero, notVolatile);

    putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags);
    volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad |
                                                 Assembler::StoreStore));
    __ jmp(Done);
    __ bind(notVolatile);

    putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags);

    __ bind(Done);
 }

assembler_x86.hpp
  // Serializes memory and blows flags
  void membar(Membar_mask_bits order_constraint) {
    // We only have to handle StoreLoad
    // x86平台只需要处理StoreLoad
    if (order_constraint & StoreLoad) {

      int offset = -VM_Version::L1_line_size();
      if (offset < -128) {
        offset = -128;
      }

  // 下面这两句插入了一条lock前缀指令: lock addl $0, $0(%rsp) 
  lock(); // lock前缀指令
  addl(Address(rsp, offset), 0); // addl $0, $0(%rsp) 
}

}

lock前缀指令的作用

  1. 确保后续指令执行的原子性。在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。
  2. LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排序。
  3. LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新store buffer的操作会导致其他cache中的副本失效
    第三条在线程B修改完flag=false之后会立马写回到内存中并把每个用到flag属性的线程中的本地内存的数据进行失效 失效了等再次用到的时间就会去内存中重新获取 这样就解决了可见性问题 如果没加lock前缀指令 修改完数据是不会立马写回到内存的会在这个变量成为无用的时候写回

上下文切换

Thread.yield();这个yield方法会让当前现在放弃所持有的时间片这样别的线程就会获取到时间片 这样的切换过程就是上下文切换 首先yield方法执行了但是这个线程中的方法还没执行完它会保存当前的上下文状态比如执行到哪了 线程执行都是cpu的寄存器、缓存、计算器等硬件实现 谁有时间片谁的数据就会放在cpu中 放弃了时间片因为cpu的寄存器、缓存空间很小所以会把没有时间片的线程数据给删除 这样当线程A放弃了时间片又得到了时间片会从内存中重新获取数据这样就解决了可见性问题

jmm模型图 与 上下文切换思维图

内存交互操作
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:

lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。

unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。

write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/606591.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号