JMM之可见性介绍

简介: JMM之可见性介绍

一、Java内存模型

JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。

JMM 体现在以下几个方面

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

简单的说,JMM 定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。

二、不可见性导致的问题

示例

main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止

import lombok.extern.slf4j.Slf4j;
import static site.weiyikai.concurrent.utils.Sleeper.sleep;
/**
 * Created by xiaowei
 * Date 2022/10/27
 * Description 退不出的循环
 */
@Slf4j(topic = "c.Test06")
public class Test06 {
    static boolean run = true;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (run){
            }
        });
        t.start();
        sleep(1);
        log.debug("停止t线程...");
        run = false; // 线程t不会如预想的停下来
    }
}

运行结果

从结果可以看出,当run变量为false后,t线程并未退出循环。

问题分析

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

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

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

三、可见性

3.1 volatile(易变关键字)

它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。

import lombok.extern.slf4j.Slf4j;
import static site.weiyikai.concurrent.utils.Sleeper.sleep;
/**
 * Created by xiaowei
 * Date 2022/10/27
 * Description 退不出的循环 -- volatile解决办法
 */
@Slf4j(topic = "c.Test06")
public class Test06 {
    volatile static boolean run = true;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (run){
            }
        });
        t.start();
        sleep(1);
        log.debug("停止t线程...");
        run = false;
    }
}

运行结果

21:49:15.174 c.Test06 [main] - 停止t线程...

线程t在1s后正常退出了循环。

分析

当主线程修改主存中的run变量的时候,t线程一直访问的是自己缓存的run值,所以不认为run已经改为false,顾不会退出循环。

当为主存(成员变量)进行volatile修饰,增加变量的可见性, 当主线程修改run为false, t线程对run的值可见,这样就可以正常退出循环。

3.2 synchronized的可见性

synchronized的执行内部代码的过程分为五步,分别是:

  • 1、获得同步锁;
  • 2、清空工作内存;
  • 3、在主内存中拷贝最新变量的副本到工作内存;
  • 4、执行代码(计算或者输出等);
  • 5、将更改后的共享变量的值刷新到主内存中;
  • 6、 释放同步锁。
import lombok.extern.slf4j.Slf4j;
import static site.weiyikai.concurrent.utils.Sleeper.sleep;
/**
 * Created by xiaowei
 * Date 2022/10/27
 * Description 退不出的循环 -- synchronized解决办法
 */
@Slf4j(topic = "c.Test07")
public class Test07 {
    static boolean run = true;
    private final static Object lock = new Object();
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (run) {
                synchronized (lock) {
                    if (!run){
                        break;
                    }
                }
            }
        });
        t.start();
        sleep(1);
        log.debug("停止t线程...");
        synchronized (lock){
            run = false;
        }
    }
}

运行结果

22:09:18.867 c.Test07 [main] - 停止t线程...

3.3 print打印输出

当在while循环代码中加入print打印输出时,t线程会退出循环。

import lombok.extern.slf4j.Slf4j;
import static site.weiyikai.concurrent.utils.Sleeper.sleep;
/**
 * Created by xiaowei
 * Date 2022/10/27
 * Description 退不出的循环 -- print打印输出
 */
@Slf4j(topic = "c.Test06")
public class Test06 {
    static boolean run = true;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (run){
                System.out.print("");
            }
        });
        t.start();
        sleep(1);
        log.debug("停止t线程...");
        run = false; // 线程t不会如预想的停下来
    }
}

运行结果

22:10:28.651 c.Test06 [main] - 停止t线程...

分析

从print的源码中可以看出,print方法使用到了synchronized,synchronized可以保证原子性、可见性、有序性。当使用synchronized后,就会将线程的工作内存清空,在主内存中拷贝最新变量的副本到工作内存。所以会获取到正确的run值。

四、可见性 vs 原子性

前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线 程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况。

上例从字节码理解是这样的:

getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
putstatic run // 线程 main 修改 run 为 false, 仅此一次
getstatic run // 线程 t 获取 run false

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

// 假设i的初始值为0
getstatic     i     // 线程2-获取静态变量i的值 线程内i=0
getstatic     i     // 线程1-获取静态变量i的值 线程内i=0
iconst_1         // 线程1-准备常量1
iadd             // 线程1-自增 线程内i=1
putstatic     i     // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1         // 线程2-准备常量1
isub             // 线程2-自减 线程内i=-1
putstatic     i     // 线程2-将修改后的值存入静态变量i 静态变量i=-1

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

总结

volatile关键字可以保证可见性、有序性,但是不能保证原子性。volatile具体原理见:volatile原理

synchronized关键字可以保证可见性、原子性,对于有序性:使用synchronized并不能解决有序性问题,但是如果是该变量整个都在synchronized代码块的保护范围内,那么变量就不会被多个线程同时操作,也不用考虑有序性问题!在这种情况下相当于解决了重排序问题!。

目录
相关文章
|
4月前
|
安全 Java 开发者
探索Java内存模型:可见性、有序性和并发
在Java的并发编程领域中,内存模型扮演了至关重要的角色。本文旨在深入探讨Java内存模型的核心概念,包括可见性、有序性和它们对并发实践的影响。我们将通过具体示例和底层原理分析,揭示这些概念如何协同工作以确保跨线程操作的正确性,并指导开发者编写高效且线程安全的代码。
|
6月前
|
安全 Java
7.volatile怎么通过内存屏障保证可见性和有序性?
7.volatile怎么通过内存屏障保证可见性和有序性?
61 0
7.volatile怎么通过内存屏障保证可见性和有序性?
|
Java 编译器 程序员
JMM的内存可见性保证
JMM的内存可见性保证
52 0
|
存储 缓存 Java
到底什么是内存可见性?
到底什么是内存可见性?
147 0
|
存储 SQL 缓存
|
缓存 Java
内存可见性引发的思考
内存可见性引发的思考
内存可见性引发的思考
|
存储 安全 Java
JMM高并发详解(java内存模型、JMM三大特征、volatile关键字 )
JMM高并发详解(java内存模型、JMM三大特征、volatile关键字 )
JMM高并发详解(java内存模型、JMM三大特征、volatile关键字 )
volatile与JMM(二)
问:volatile凭什么可以保证可见性和有序性 答: 内存屏障
100 0
volatile与JMM(二)
|
Java 编译器
【多线程:volatile】可见性
【多线程:volatile】可见性
141 0