深入理解 Java 虚拟机:Java 内存模型与线程

    技术2022-07-13  73

    深入理解 Java 虚拟机:Java 内存模型与线程

    什么是内存模型?什么是高速缓存?缓存一致性Java 内存模型主内存工作内存内存间的交互操作volatile 关键字对所有线程可见性禁止指令重排序 Java 内存模型的特征原子性可见性有序性synchronized 缺点 先行发生原则“天然的” 先行发生关系 Java 线程状态

    什么是内存模型?

    内存模型: 可以理解为,在特定的操作协议下,对特点的内存或高速缓存进行读写访问的过程的抽象。

    什么是高速缓存?

    在运算时,将需要使用到的数据从内存复制到缓存(Cache)中,以此让计算能更快的进行,计算结束后再从缓存同步回内存中,这样就无需频繁的等待缓慢的内存读写。

    缓存一致性

    缓存一致性: 在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存。

    Java 内存模型

    主内存

    主内存是虚拟机内存的一部分。

    Java 内存模型规定所的变量都存储在主内存中。

    变量数据: 最根源的数据,线程不能直接操作。

    工作内存

    工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等),都必须在工作内存中进行,不能直接读写主内存中的数据。不同线程间也无法直接访问对方工作线程中的变量。

    变量数据: 从主内存中拷贝来的副本,其他线程无法访问,操作结束后需写回主内存。

    内存间的交互操作

    虚拟机实现时,必须保证下面提及的每一种操作都是原子的、不可再分的。

    lock(锁定): 作用于主内存的变量,把变量标记为某个线程独占。unlock(解锁): 作用于主内存的变量,释放一个锁定的变量。read(读取): 作用于主内存的变量,把一个变量的值从主内存传输到工作内存中,以便后续 load 动作使用。load(载入): 作用于工作内存的变量,将 read 过来的值放入工作内存的变量副本中。use(使用): 作用于工作内存的变量,把 工作内存 中存在的一个变量传递给执行引擎,每当虚拟机遇到一个需要使用变量的值时就会执行这个操作。assign(赋值): 作用于工作内存的变量,把从执行引擎接收到的值,赋值给工作内存中的一个变量,当虚拟机遇到赋值操作时候执行操作。store(存储): 作用于工作内存的变量,把工作内存中的一个变量传递给主内存,一遍后续 write 动作使用。write(写入): 作用于主内存的变量,把 store 操作的值放入主内存的变量中。

    将变量从主内存复制到工作内存: 顺序执行 read 和 load,只需按顺序即可,中间可插入其他指令。

    从工作内存写回主内存: 顺序执行 sore 和 write,只需按顺序即可,中间可插入其他指令。

    volatile 关键字

    对所有线程可见性

    特性: 保证此变量对所有线程的可见性,当一条线程修改了这个变量值,其他线程会立即收到通知。

    volatile 关键字保证线程可见,不代表线程安全: 以下为 race ++ 的反编译结果 volatile 关键字 保证了线程可见性,因此只要去读数据,就可以拿到正确的数据

    而这里的读操作,只有 getstatic 字节码指令,将数据读入栈顶(每个线程各自拥有,对外部可见)

    而后续的不管是递增 iadd 还是回写回主内存 putstatic 指令之前都没有再去读数据

    因此如果这期间如果数据发生了变更,在栈顶的数据就成了旧数据,回写回主内存就会产生覆盖,线程不安全。

    禁止指令重排序

    普通变量仅仅会保证在该方法的执行过程中,所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

    这么做的目的是一种机器级的优化操作,使得某个部分的汇编代码被提前执行。

    volatile 可以避免这种的优化操作。

    内存屏障: volatile 是通过 内存屏障 来防止指令重排序的,反编译后即下图红色框框中的部分。

    作用: 防止 内存屏障 后面的指令排序到 内存屏障 之前。

    Java 内存模型的特征

    Java 内存模型是围绕 并发过程中 如何处理 原子性、可见性 和 有序性 3 个特征来建立的。

    原子性

    直接 保证原子性的操作 包括:read、load、assign、use、store、write

    基本数据类型的访问读写都是具备原子性的 例外的只有 long 和 double,具有非原子协定,但是各大虚拟机实现时,都把他们的读写操作作为原子操作来对待,因此知道下就好了,不需要太在意。

    为了保证原子性,虚拟机提供字节码 monitorenter 和 monitorexit 来操作 lock 和 unlock,反应到代码层面就是 synchronized 关键字,因此 synchronized 块的操作也具有原子性。

    可见性

    可见性: 可见性是指,当一个线程修改了共享变量的值,其他变量能立即得知这个修改。

    保证可见性的关键字: volatile、synchronized、final。

    有序性

    有序性: 如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无需的。前半句是指 “线程内表现为串行的语义”,后半句是指 “指令重排序” 现象和 “工作内存和主内存同步延迟” 现象。

    防止指令重排序关键字: volatile、synchronized。

    synchronized 缺点

    synchronized 同时满足 3 大特性,看起来十分 “万能”,但过度滥用回导致性能问题。

    先行发生原则

    即多线程同时操作一个变量导致值不正确的线程不安全问题。

    “天然的” 先行发生关系

    即不需要同步器,在代码中能直接使用

    程序次序规则: 在一个线程内,按照程序代码的顺序,写在前面的代码比写在后面的代码先执行。管程锁定规则: 同一个锁,一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。不解锁就没法再锁定。volatile 变量规则: 对一个 volatile 变量的写操作优先于后面这个变量的读操作,“后面” 是指时间上的先后顺序。线程启动规则: Thread 对象的 start() 方法先行发生于此线程的每个动作。不启动线程,其他动作无法进行。线程终止规则: 线程中所有操作都优先于此线程的终止检查,可通过 Thread.join() 方法结束 、Thread.isAlive() 返回值 等手段检查到线程已经终止执行。线程中断规则: 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检查到终端事件的发生,可通过 Thread.interrupt() 方法检查到是否有中断发生。对象终结规则: 一个对象的初始化完成(构造函数执行结束),先行发生于它的 finalize() 方法的发送传递性: 操作 A 优先于操作 B,操作 B 优先于操作 C,操作 A 就必然优先于 操作 C

    Java 线程状态

    新建(New): 创建后 尚未启动 的线程状态运行(Runable): 包含了操作系统线程状态中的 Runable 和 Ready 状态,处于此状态下的线程可能处于 正在执行 或者 等待 CPU 分配执行事件。无期限等待(Waiting): 此状态的线程 不会被分配执行事件,需要 等待被其他线程唤醒。以下方法会让线程进入 Waiting。 没有设置 Timeout 参数的 Object.wait() 方法。 没有设置 Timeout 参数的 Object.join() 方法。 LockSupport.park() 方法。限期等待(Time Waiting): 此状态下线程不会被分配 CPU 执行时间,不过无需其他线程显式的唤醒,在一定时间后由系统自动唤醒。以下方法会让线程进入 Time Waiting。 Thread.sleep()。 设置 Timeout 参数的 Object.wait() 方法。 设置 Timeout 参数的 Object.join() 方法。 LockSupport.parkNanos() 方法。 LockSupport.parkUnit() 方法。阻塞(Blocked): “阻塞状态” 在等待获取一个 排他锁,需要 另一个线程放弃这个锁的时候发生。结束(Terminated): 已终止 线程的状态,现在已经 结束执行。
    Processed: 0.011, SQL: 9