Java学习内存模型以及线程安全的可见性问题(八)

上次线程池已经说过了,从今天开始一起了解下JVM内存模型详解。

(一)容易误解的部分

老铁很容易把JAVA的内存区域、JAVA的内存模型,GC分代回收的老年代和新生代也容易搞混,绕进去绕不出来。学习多线程之前一定要搞明白这些问题,可能在你的内心一直认为多线程就是一个工具,所有的底层都是C++来写的,没办法去看,为什么要有java,java其实就是屏蔽了底层的复杂性。

  • ① GC内存区域

堆的概念,老年代,新生代,Eden,S0,S1

  • ② JAVA的内存区域

JVM运行时的区域:java编译生成class,线程共享部分(方法区,堆内存),线程独占部分(虚拟机栈,本地方法栈,程序计数器)

  • ③ JAVA的内存模型(概念)

针对多核多CPU,多线程而制定的一套规范规则,不是一种开发技术。

(二)多线程中的问题

  1. 所见非所得(你看到的并不是所想的)、
  2. 无法肉眼去检测程序的准确性(多线程下,完全看不出来正常不正常)。
  3. 不同的运行平台有不同的表现。
  4. 错误很难重现。

(三)工作内存和主内存

  • ① 主内存

创建一个对象在堆里面,也可以称之为主内存,不仅仅是在堆,存在一个对象X,就存在主内存

  • ② 工作内存

线程运行在工作内存, 虚拟机栈,程序计数器,CPU,高速缓存。

工作内存和主内存只是一个逻辑上的划分,概念上的东西。

  • ③ 奇妙的现象

主内存的flag传输到工作内存flag的时候,存在CPU缓存的情况,CPU缓存可能导致非常短的时间内不一致,本身CPU厂家底层是要做一致处理的,但是存在短时间内的不一致。

(四)指令重排

  • ① 介绍

Java语言规范JVM线程内部维持顺序或语义,即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。

  • ② 意义

使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率。

重排序,只能保证单个线程的,如果是多线程的话,就没有爆发保证重排序。

// 线程1 
a = d; b = 2
// 线程2 
c = a; d =3

//重排序后
//线程1 
b = 2 ; a =d;
//线程2
d = 3 ; c =a;

编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

(五) 如何不进行指令排序

  • ① 介绍

The Java volatile keyword is used to mark a Java variable as "being stored in main memory". More precisely that means, that every read of a volatile variable will be read from the computer's main memory, and not from the CPU cache, and that every write to a volatile variable will be written to main memory, and not just to the CPU cache。 CPU不缓存。

  • ② 实例
public class VisibilityDemo2 {
    // 状态标识 (不用缓存)
    private volatile boolean flag = true;

    // 源码 -> 字节码class
    // JVM 转化为 操作系统能够执行的代码 (JIT Just In Time Compiler 编译器 )(JVM  --  client   , --server)
    public static void main(String[] args) throws InterruptedException {
        VisibilityDemo2 demo1 = new VisibilityDemo2();
        new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (demo1.flag) {
                    i++;
                }
                System.out.println(i);
            }
        }).start();

        TimeUnit.SECONDS.sleep(2);
        // 设置is为false,使上面的线程结束while循环
        demo1.flag = false;
        System.out.println("被置为false了.");
    }
}

不添加volatile ,就不会打印i的值。

(六) 内存模型

  • ① 介绍

内存模型描述程序的可能行为。JAVA编程语言内存模型通过检查执行跟踪中的每个读操作,并根据某些规则检查该操作观察到的写操作是否有效来工作。

只要程序的所有执行产生的结果都可以由内存模型预测,具体的实现者任意实现,包括操作的重新排序和删除不必要的同步。

内存模型决定了在程序的每个点上可以读取什么值。

  • ② 共享变量描述

可以在线程之间共享的内存称为共享内存或堆内存。所有实例字段,静态字段和数组元素都存储在堆内存中。如果至少有一个访问是写的,那么对同一个变量的两次访问(读或写)是冲突的。线程1修改过共享变量后,将共享变量刷到主内存,然后,线程2从主内存读取该共享变量,将该共享变量载入到工作内存中。

  • ③ 线程操作的定义
  1. write要写的变量以及要写的值。
  2. read 要读的变量以及可见的写入值(由此,我们可以确定可见的值)。
  3. lock 要锁定的管程。
  4. unlock 要解锁的管程。
  5. 外部操作(socket等等)。
  6. 启动和终止。

如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的。这是重排序必须要遵守的规则。

(七)对于同步规则的定义

  • ① 对于监视器m的解锁与所有后续操作对于m的加锁同步

synchronized 在同步关键字,在内存中都明确定义了,保持可见,及时反馈给主内存中。一环扣一环,想可见,必须反馈到主内存。既有同步的语义,还有保持可见性的功能。


import java.util.concurrent.TimeUnit;

public class VisibilityDemo1 {
    // 状态标识
    private static boolean is = true;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (VisibilityDemo1.is) {
                    synchronized (this) {
                        i++;
                    }
                }
                System.out.println(i);
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 设置is为false,使上面的线程结束while循环
        VisibilityDemo1.is = false;
        System.out.println("被置为false了.");
    }
}

  • ② 对volatile 变量v的写入,与所有其他线程后续对 v 的读同步

变量标识了volatile ,后面不管哪个线程来读,都是同步的,都是可见的。

public class VisibilityDemo {
    private volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        VisibilityDemo demo1 = new VisibilityDemo();
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                // class ->  运行时jit编译  -> 汇编指令 -> 重排序
                while (demo1.flag) { // 指令重排序
                    i++;
                }
                System.out.println(i);
            }
        });
        thread1.start();

        TimeUnit.SECONDS.sleep(2);
        // 设置is为false,使上面的线程结束while循环
        demo1.flag = false;
        System.out.println("被置为false了.");
    }
}

  • ③ 启动线程的操作与线程中的第一个操作同步
  • ④ 对于每个属性写入默认值(0,false,null)与 每个线程对其操作的同步

  • ⑤ 线程T1的最后操作与线程T2发现线程T1已经结束同步(isAlive,join可以判断线程是否终结)

  • ⑥ 如果线程T1终端了T2,那么线程T1的中断操作与其他所有线程发现T2倍中断了同步,通过抛出InterruptedException异常,或者调用Thread.interrupted 或者 Thread.isInterrupted。

(八)Happyens-before先行发生原则

  • ① 介绍

强调两个有冲突的动作之间的顺序,以及定义数据征用的发生时机。

  • ② 原则
  1. 同一个线程里面对数据做了变动,后面的动作可以及时的看到,其实还是可见性。
  2. 某个monitor上的unlock动作 happens-before 同一个monitor上后续的lock动作。
  3. 对某个volatile 字段的写操作 happens-before 每个后续对该 volatile 字段的读操作。
  4. 在某个线程对象上调用start() 方法 happens-before 该启动了的线程中的任意动作。
  5. 某个线程中的所有动作 happens-before 任意其他线程成功从该线程对象上的join() 中返回。
  6. 如果某个动作 a 在happens-before 动作 b,b 在happens-before 动作 c,则 a happens-before c。

(九) final 在JMM中的处理

  • ① final在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的final字段的正确构造版本。
  • ② 如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则它将看到默认值。

  • ③ 读取该共享对象的final成员变量之前,先要读取共享对象。

  • ④ 通常static final 是不可以修改的字段。然而System.in, System.out 和 System.err 是static final 字段,遗留原因,必须允许通过set方法改变,这些字段称为写保护,以区别于普通的final字段。

PS:使用了volatile,unlock和lock的时候,就可以保证代码不进行重排序。内存模型java进阶的一个核心点,这个理解了,其实比写多少年的业务代码要重要很多。

你可能感兴趣的:(Java学习内存模型以及线程安全的可见性问题(八))