004. 线程安全之可见性问题

橙三吉。 提交于 2019-12-25 13:14:29

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

1. Java 内存内存模型 vs JVM 运行时数据区


img

2. 初看 Java 内存模型


  • 多线程程序语义:当多个线程修改了共享内存中的值时,应该读取到哪个值的规则。这些语义没有规定如何执行多线程程序,相反,他们描述了允许多线程程序的合法行为。
  • 说白了,Java 内存模型实际上描述的是 Java 语言在执行多线程程序时的一些规则。

3. 多线程中的问题


  1. 所见非所得
  2. 无法肉眼去检测程序的准确性
  3. 不同的运行平台有不同的表现
  4. 错误很难重现
public class Demo10 {
    int i = 0;
    boolean isRunning = true;

    public static void main(String[] args) throws InterruptedException {
        Demo10 demo = new Demo10();

        new Thread(() -> {
            System.out.println("here i am ...");
            while (demo.isRunning) {
                demo.i++;
            }
            System.out.println(demo.i);
        }).start();

        Thread.sleep(3000);
        demo.isRunning = false;
        System.out.println("shutdown ...");
    }
}
  • 可以通过配置指定 VM options

<img src="https://oscimg.oschina.net/oscnet/up-2a07b0064afd2e83b0df8f53c6c48cd6719.png" width="700"/>

参数 32 位 JDK 64 位 JDK
-server 不打印i的值 不打印i的值
-client 打印i的值 不打印i的值

4. 工作内存缓存


<img src="https://oscimg.oschina.net/oscnet/up-ba96a2acfd74d39c2bc220649f8c8d360b0.png" width="700"/>

  • data 数据在两个线程间的同步问题。

5. JIT 编译(Just In Time Compiler)


1. 脚本语言和编译语言的区别

  • 解释执行:即咱们说的脚本,在执行时,有语言和解释器将其一条条翻译成机器可识别的指令。
  • 编译执行:将我们编写的程序,直接编译成机器可以识别的指令码。

2. Java 是脚本语言还是编译语言?

  • Java 介于脚本语言与编译语言之间。

img

3. 针对上面程序的问题解决办法

  • isRunning添加volatile关键字即可。
volatile boolean isRunning = true;

6. volatile 关键字


  • 可见性问题:让一个线程对共享变量的修改,能够及时地被其他线程看到。
  • Java内存模型规定:对 volatile 变量 v 的写入,与所有其他线程后续对 v 的读同步。

volatile 关键字功能

7. Shared Variables 定义


  • 可以在线程之间共享的内存称为共享内存或堆内存。
  • 所有实例字段、静态字段和数组元素都存储在堆内存中,这些字段和数组都是标题中提到的共享变量。
  • 冲突:如果至少有一个访问是写操作,那么对同一个变量的两次访问是冲突的。
  • 这些能被多个线程访问的共享变量是内存模型规范的对象。
  • 定义在:https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.1

8. 线程间操作的定义


  1. 线程间操作指:一个程序执行的操作可被其他线程感知或被其他线程直接影响。
  2. Java 内存模型只描述线程间操作,不描述线程内操作,线程内操作按照线程内语义执行。

线程间操作有:

  • read 操作(一般读,即非 volatile 读)
  • write 操作(一般写,即非 volatile 写)
  • volatile read
  • volatile write
  • Lock.(锁 monitor)、Unlock
  • 线程的第一个和最后一个操作
  • 外部操作

9. 对于同步的规则定义


  • 对 volatile 变量 v 的写入,与所有其他线程后续对 v 的读同步。
  • 对于监视器 m 的解锁与所有后续操作对于 m 的加锁同步。
  • 对于每个属性写入默认值(0, false, null)与每个线程对其进行的操作同步。
  • 启动线程的操作与线程中的第一个操作同步。
  • 线程T2的最后操作与线程T1发现T2已经结束同步。(isAlive,join可以判断线程是否终结)
  • 如果线程T1中断了T2,那么线程T1的中断操作与其他所有线程发现T2被中断了同步。通过抛出 InterruptedException 异常,或者调用 Thread.interrupted 或 Thread.isInterrupted。

10. Happens-before 先行发生原则


  • happens-before 关系用于描述两个有冲突的动作之间的顺序,如果一个 action happens before 另一个 action,则第一个操作被第二个操作可见,JVM 需要实现如下 happens before 规则:

    • 某个线程中的每个动作都 happens-before 该线程中该动作后面的动作。
    • 某个管程上的 unlock 动作 happens-before 同一个管程上后续的 lock 动作。
    • 在某个 volatile 字段的写操作 happens-before 每个后续对该 volatile 字段的读操作。
    • 在某个线程对象上调用 start() 方法 happens-before 被启动线程中的任意动作。
    • 如果在线程 t1 中成功执行了 t2.join(),则 t2 中的所有操作对 t1 可见。
    • 如果某个动作 a happens-before 动作 b,且 b happens-before 动作 c,则有 a happens-before c。
  • 当程序包含两个没有被 happens-before 关系排序的冲突访问时,就称存在数据竞争。

  • 遵守了这个原则,也就意味着有些代码不能进行重排序,有些数据不能缓存。

11. final 在 JMM 中的处理


  • final 在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的 final 字段的正确构造版本。

    • 伪代码示例:f=new finalDemo(); 读取到的 f.x 一定最新,x 为 final 字段。
  • 如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则他将在看到默认值。

    • 伪代码示例:public finalDemo() {x=1;y=x;}; y会等于1。
  • 读取该共享对象的final成员变量之前,先要读取共享对象。

    • 伪代码示例:r=new ReferenceObj(); k=r.f; 这两个操作不能重排序。
  • 通常被static final修饰的字段,不能被修改。然而System.inSystem.outSystem.err被static final修饰,却可以修改,遗留问题,必须允许通过set方法修改,我们将这些字段称为写保护,以区别于普通的final字段。

    <img src="https://oscimg.oschina.net/oscnet/up-f133a960110d3ad875503e282867080e111.png" width="400"/>

    <img src="https://oscimg.oschina.net/oscnet/up-72d5ed20299b069c63f84ef9edb43825384.png" width="400"/>

12. Word Tearing字节处理


  • 有些处理器(尤其是早起的Alphas处理器)没有提供写单个字节的功能。在这样的处理器上更新byte数组,若只是简单地读取整个内容,更新对应的字节,然后将整个内容再写回内存,将是不合法的。
  • 这个问题有时候被称为“字分裂(word tearing)”,更新单个字节有难度的处理器,就需要寻求其他方式来解决问题。
  • 因此,编程人员需要注意,尽量不要对 byte[] 中的元素进行重新赋值,更不要在多线程程序中这样做。

13. double 和 long 的特殊处理


  • 由于《Java语言规范》的原因,对非 volatile 的 double、long 的单次写操作是分两次来进行的,每次操作其中 32 位,这可能导致第一次写入后,读取的值是脏数据,第二次写完成后,才能读到正确值。

    <img src="https://oscimg.oschina.net/oscnet/up-133fd652edb772e142e17713a7a33dfdec1.png" width="300"/>

  • 商业JVM不会存在该问题,虽然规范没要求实现原子性,但是考虑到实际应用,大部分都实现了原子性。

  • 读写volatile修饰的long、double是原子性的。

  • 《Java语言规范》中说道:建议程序员将共享的64位值(long、double)用volatile修饰或正确同步其程序以避免可能的复杂的情况。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!