并发编程模型的两个关键问题:线程之间如何通信及线程之间如何同步

    Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。一个线程改变共享变量需要改变本地内存副本,然后更新主内存中共享变量的值,然后另一个线程使用此共享变量需要到主内存中拉取,从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。

    重排序问题:

    • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
    • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
    • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

    Happens-Before规则:前面一个操作的结果对后续操作是可见的,Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens- Before 规则。

    1. 程序的顺序性规则:在一个线程中,前一个操作先于后续的任意操作
    2. volatile变量规则:volatile变量的写操作对于后续的读操作是可见的
    3. A Happens-Before B,B Happens-Before C,A Happens-Before C
    4. 管程中锁的规则:对一个锁的解锁对于这个锁的加锁是可见的(管程就是synchronized)
    5. 线程start()规则:主线程启动子线程,子线程能够看到主线程在启动子线程之前的操作
    6. 线程join()规则:主线程等待子线程完成调用join()方法,主线程能看到子线程中对于共享变量的修改

    happens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。

    重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。编译器和处理器在重排序时,会遵 守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

    as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程) 程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。 但是对于多线程是不适用的,对存在控制依赖的操作重排序,可能会改变程序的执行结果

    volatile变量:

    • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
    • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

    volatile写和volatile读的内存语义做个总结:

    1. 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
    2. 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
    3. 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

    基于保守策略的JMM内存屏障插入策略:

    • 在每个volatile写操作的前面插入一个StoreStore屏障。(不允许上边的普通写操作和下边volatile写操作重排序)
    • 在每个volatile写操作的后面插入一个StoreLoad屏障。(不允许上边volatile写操作和下边普通读写操作重排序)
    • 在每个volatile读操作的后面插入一个LoadLoad屏障。(不允许上边volatile读操作和下边普通和volatile读操作重排序)
    • 在每个volatile读操作的后面插入一个LoadStore屏障。(不允许上边volatile读操作和下边普通写操作重排序)

    由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以 确保对整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大;在可伸缩性和执行 性能上,volatile更有优势。

    锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

    对锁释放和锁获取的内存语义做个总结:

    • 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
    • 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
    • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

    Java的compareAndSet()方法调用简称为CAS,如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。

    CAS同时具有volatile读和volatile写的内存语义:

    公平锁和非公平锁的内存语义做个总结。

    ·公平锁和非公平锁释放时,最后都要写一个volatile变量state。

    ·公平锁获取时,首先会去读volatile变量。

    ·非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile

    写的内存语义。

    锁释放-获取的内存语义的实现:

    1. 利用volatile变量的写-读所具有的内存语义。
    2. 利用CAS所附带的volatile读和volatile写的内存语义。

    由于Java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信现 在有了下面4种方式。

    1. A线程写volatile变量,随后B线程读这个volatile变量
    2. A线程写volatile变量,随后B线程用CAS更新这个volatile变量
    3. A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量
    4. )A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量

    final域,编译器和处理器要遵守两个重排序规则。

    1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

    2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能 重排序。

    123