JUC并发编程 共享模型之内存 -- Java 内存模型 & 原子性 & 可见性(可见性问题及解决 & 可见性 VS 原子性 & volatile(易变关键字))(代码

Posted Z && Y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC并发编程 共享模型之内存 -- Java 内存模型 & 原子性 & 可见性(可见性问题及解决 & 可见性 VS 原子性 & volatile(易变关键字))(代码相关的知识,希望对你有一定的参考价值。

1. Java 内存模型

JMM 即 Java Memory Model,它定义了主存(所有线程都共享的数据,包括成员变量)、工作内存(线程私有的数据,包括局部变量)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。

JMM 体现在以下几个方面:

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

2. 原子性

Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性


3. 可见性


3.1 可见性问题

退不出的循环:

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

示例代码:

package tian;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test32")
public class Test32 {
    // 易变
    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                if (!run) {
                    break;
                }
            }
        });
        t.start();
        
        run = false; // 线程t不会如预想的停下来
    }
}

问题原因:

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。

  2. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率

  3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值


3.2 可见性问题解决


3.2.1 解决方法1( 推荐 ): volatile(易变关键字)

volatile(易变关键字):

  • 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。
  • volatile 关键字可以保证共享变量在多个线程之间的可见性

3.2.2 解决方法2: synchronized

package tian;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test32")
public class Test32 {
    static boolean run = true;
    static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    if (!run) {
                        break;
                    }
                }
            }
        });
        t.start();

        Thread.sleep(1000);
        synchronized (lock) {
            run = false;
        }
    }
}

3.2 可见性 VS 原子性

前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况: 上例从字节码理解是这样的:

比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解决指令交错

注意:

synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低。



以上是关于JUC并发编程 共享模型之内存 -- Java 内存模型 & 原子性 & 可见性(可见性问题及解决 & 可见性 VS 原子性 & volatile(易变关键字))(代码的主要内容,如果未能解决你的问题,请参考以下文章

并发编程(学习笔记-共享模型之JUC-读写锁原理)-part6

JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕( 可以覆盖上次的打印内)等待多个远程调用结束)(代码片段

并发编程(学习笔记-共享模型之内存)-part4

JUC学习之共享模型之内存

JUC学习之共享模型之内存

java并发编程之volatile