Java并发——Java内存模型

本篇博文是Java并发编程实战的笔记。

本书的最后一章~~~,但基本上参考Java官方文档比这本书多...

内存模型?#

现代CPU为了提升性能常常会做一些优化,比如CPU将本来有序的指令重排,以乱序的方式执行;比如CPU将内存中的信息缓存到每个核心独有的寄存器或高速缓存......Java编译器(以及大部分现代编译器)也会做类似的事。

卧槽!那你们挺狗啊,你们自己干活倒是快了,留下一堆问题给程序员,在这种情况下,我们应该如何保证不同线程之间操作的可见性?关于可见性问题,可以看这篇文章Java并发——共享对象

不过,各种架构的CPU都定义了自己的内存模型,它们告诉程序员它们可以从内存系统中获得怎样的存储一致性保证,并且提供了一些特殊的同步指令(比如内存栅栏)来帮助程序员协调并发执行。但是不同架构的硬件提供的内存模型不一样,程序员还是头疼的。为此,JMM(Java内存模型)提供了自己的内存模型,来屏蔽这些烦人的底层差异,Java开发者只需要遵循JMM模型即可,至于其它的,交给JVM。

Programs and Program Order#

JMM定义的这个原则不知道怎么翻译...

对于每一个线程t,在它内部执行的所有操作的顺序是一个总顺序,反映了根据线程t中的语义进行执行这些操作的顺序。有点绕,意思就是在一个单一线程内部,不管你怎么重排序,你都必须保证它们最后的执行结果和没重排过一样。在单一线程中,每一个对于变量v的读取操作r,必须可以看到之前对变量v的写入操作w写入的值。

同步顺序原则(Synchronization Order)#

上面的一条原则让指令重排在单线程中看起来像没重排过一样,JMM中也定义了几条同步顺序原则,即在这几条原则下两个可能来自不同线程的操作看起来是同步顺序的,即看起来没被重排过(就是某个操作的顺序一定会先于另一个操作)

  1. 对于同一个锁,一个解锁操作将与后续任何线程的加锁操作保持同步顺序(即不会有一个线程还没完成解锁操作,另一个线程就加了锁)
  2. 对一个volatile变量的写入与后续任何线程对它的读取保持同步顺序
  3. 线程的start操作与该线程中第一行语句的保持同步顺序
  4. 变量默认值的写入与每个线程的第一个动作保持同步顺序
  5. 线程T1中的最后一个操作与另一个线程T2检测到线程T1已经终结保持同步顺序
  6. 如果T1打断了T2,那么T1的中断与任何线程(包括T2)检测到T2已经被中断保持同步顺序

Happens-Before Order#

JMM中定义了几条Happens-Before原则,提供了并发线程间可见性的保证,想要A中的操作对B可见,那么A中的操作必须Happens-Before B,否则,JVM并不对两个线程之间的可见性做任何保证。

  1. 对于同一个锁,一个解锁操作在后面的一个加锁操作之前发生

    sychronzied(object1) {
        // dosomething...
    }
    

    如果线程A先解锁了这个代码块,之后线程B加锁,线程A对内存所做的所有操作对线程B可见

  2. 一个写入操作在后面一个读取操作之前发生

    volatile int age = 20;
    

    对于一个这种变量,若线程A先写入它,线程B后读取它,线程A写入的值对B可见

  3. 一个线程的start方法先于线程中的每一个动作

    // do something...
    // ...
    // Now in Thread-A
    new Thread(task, "Thread-B").start();
    

    假设上面,线程A启动了一个线程B,那么在线程B中,线程A调用start方法前的所有操作对线程B可见

  4. 线程中的所有操作都先行发生于对它的join操作返回之前

    // Now in Thread-A
    Thread t = new Thread(() -> {
        // Thread-B doSomething...
    }, "Thread-B");
    t.start();
    
    t.join();
    // Thread-A doSomething...
    

    如上面的例子,在线程A的t.join返回之后,线程B中所做的所有操作对线程A可见

  5. 何对象的默认初始化方法都先行发生于对这个对象的所有操作之前

请注意Happens-Before原则和同步顺序原则的不同,好多教程里面把它们混为一谈,同步顺序原则是用来保证某些操作重排后的执行顺序看起来像没有重排过一样,而Happens-Before则是保证如果线程A中的某些操作实际上先于B中的某些操作发生,那么线程A中所作的操作对线程B都可见。

注意,这只是我个人的理解,参考了Java Language Specification中的Chapter17 17.4.4-17.4.5,官方的说法肯定是最准确的,但是可能由于本人水平会错了意,等有时间把这份定义啃一遍。

final域语义#

被定义成final的域只初始化一次,在通常情况下它永远不会被改变。编译器可以保持将final变量缓存在寄存器中,而不用重新从主存中加载它。

final域还可以让程序员实现线程安全的不可变对象,而且不用使用任何的同步。

当一个对象的构造函数完成时,该对象才被认为是完全初始化的。一个线程只有当对象被完全初始化之后才能看到该对象的引用。对于final类型的域,能够保证其它线程看到该final字段正确初始化的值。

posted @   yudoge  阅读(39)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示
主题色彩