Java内存模型和线程

  1. 类比物理计算机的并发问题
    • 运算不可能只靠处理器的计算完成,还需要内存交互
    • 内存交互和处理器计算比起来,太慢了
    • 现代计算机系统不得不加入一层读写速度接近与处理速度的高速缓存(Cache),
    • Cache缓解了速度矛盾,但引生了新问题:缓存一致性问题
    • 每个处理器都有自己的告诉缓存,却又共享内存,不可避免的会有读写冲突
    • 因此读写缓存都有遵守一定的协议
    • 为了提高效率,处理器对代码进行乱序执行,对结果再进行重组(指令偏序执行)
  2. Java内存模型(JMM)
    • 目标:定义程序中共享变量的访问规则
    • JMM规定所有共享变量(堆上的对象等),都必须存放到主内存,每条线程都有自己的工作内存
    • 工作内存保存了线程使用到的共享变量的主内存副本拷贝 TODO 容器类是如何拷贝的?
    • 线程对变量的操作都必须在工作内存,而不能直接操作主内存
    • JMM规定了8种原子操作来完成工作内存和主内存的交互:
      • lock: 作用于主内存,标记变量为线程独占状态
      • unlock: 作用与主内存,把处于锁定状态的变量释放
      • read: 作用于主内存,将变量的值从主内存传输到工作内存,以便之后load
      • load: 作用于工作内存,把read的变量值放到工作内存的变量副本中
      • use: 作用于工作内存,将变量传递给执行引擎
      • assign: 作用于工作内存,将执行引擎输出的值赋给工作内存变量
      • store: 作用于工作内存,将变量值传递给主内存,以便之后write
      • write: 作用于主内存,将store传递来的变量写到主内存
    • 上面8中操作需满足以下条件:
      • 不允许read和load,store和write操作之一单独出现
      • 不允许丢弃assign操作
      • 不允许没有assign就把数据同步到主内存
      • 新变量只能产生于主内存,之后load到工作内存操作,之后store回去
      • lock的唯一行和可重入性
      • 执行unlock前,必须现将该变量同步回主内存(store, write)
    • volatile是JVM最轻量级的同步机制,有两个特性:
      • 可见性,volatile的变量在使用前都要从主内存中load,使用后立即刷新回主内存,普通变量并不是每次操作都和主内存同步
      • 禁止指令重排序优化(指令重排序是指代码的执行是偏序的,可以改变两个不直接依赖的两个语句的执行顺序,保证单个变量的操作是顺序的)
    • long和double的非原则性协定,即虚拟机允许没有被volatile修饰的变量的读写操作分两次32位操作来进行,即不保证64位数据的load, store, read, write操作的原子性。(在实际开发中,各平台下的商用虚拟机都已将64位数据的读写操作作为原子操作对待)
    • 原子性 vs 可见性 vs 有序性
      • 原子操作,不可中断的操作,各层次命令:lock(unlock)–monitorenter(monitorexit)–synchonized或Lock
      • 可见性,操作对所有线程即时可见,三种实现手段:
        • valotile: 变量使用前从主内存刷新,更改后同步回主内存
        • synchronized: 执行unlock前,必须现将该变量同步回主内存
        • final: 在构造器中初始化后不可变 TODO this引用逃逸
      • 有序性,在本线程内看,所有操作都是有序的(线程内表现为串行语义),从外面看,所有操作都是无序的(指令重排,工作内存和主内存同步延迟)
        • volatile,如上所述
        • synchronized,lock的唯一行和可重入性,同一个锁的两个同步块只能串行进入
TOP