【多线程:volatile】可见性

简介: 【多线程:volatile】可见性

【多线程:volatile】可见性

01.介绍

可见性是值,一个线程对共享变量修改另一个线程可以看到最新的结果

02.例子

@Slf4j(topic = "c.Test32")
public class Test32 {
    static Boolean stop = false;

    public static void main(String[] args) {
        new Thread(()->{
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            log.debug("把stop修改为true");
        }).start();

        foo();
    }
    static void foo(){
        int i=0;
        while (!stop){
            i++;
        }
        log.debug("stopped...c:{}",i);
    }
}

结果

程序没有结束:
18:44:55.991 c.Test32 [Thread-0] - 把stop修改为true

解释
上述代码表达的意思是,我们创建了一个stop成员变量默认为false 我们创建一个方法foo 它里面有一个while循环 只有stop为true时 它才能跳出循环,然后 我们创建了一个线程 我们在它里面使stop变为true,我们使线程在0.1s后再执行stop=true,所以理论上来说 我们在0.1s后 while循环就应该退出并打印循环次数了,可是我们却发现 这个循环并没有退出 也就是,对于while循环来说此时stop还是false

03.对于上述例子的解释

错误解释

在网上有这么一种解释,下图为JMM(java内存模型)图
注意:下文中的高速缓存 副本 工作内存指的是一个东西
JMM
即每个线程对共享变量进行操作时 这个线程就会把主存中的共享变量复制一份到高速缓存中,之后线程的每次操作都是在高速缓存中执行 加快效率。就像上图t0线程复制了一个副本 并修改其共享变量副本,此时t1读取的还是它自己的共享变量副本 导致读取错误。对于上述例子就是因为t0创建了一个stop的副本 然后进行了修改stop=true,然后t1执行while(!stop)时读取的还是自己的副本 也就是主存的stop=false,所以导致循环不能结束。但这个解释是错误的。

为什么上述解释是错误的

@Slf4j(topic = "c.Test32")
public class Test32 {
    static Boolean stop = false;

    public static void main(String[] args) {
        new Thread(()->{
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            log.debug("把stop修改为true");
        }).start();

        new Thread(()->{
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("{}",stop);
        }).start();

        foo();
    }
    static void foo(){
        int i=0;
        while (!stop){
            i++;
        }
        log.debug("stopped...c:{}",i);
    }
}

结果

程序没有结束:
18:55:55.991 c.Test32 [Thread-0] - 把stop修改为true
18:55:56.100 c.Test32 [Thread-1] - true

解释
我们另外创建一个线程Thread1 在Thread0线程运行之后 Thread1线程在运行 打印此时stop的结果,结果发现此时结果为true,但是如果按照上面那个解释 修改的应该是副本 那么主存中的stop=false应该是不变的,Thread1也应该读取的是自己拷贝主存的数据stop=false,但现在结果为true,说明上述理论是错误的。

正确的解释:JMM(java内存模型)

解释:还是上面那张图,不过需要加一个条件 那就是每个副本在进行写操作后都会更新主存中的数据 更新后其它线程的副本也需要更新为主存中的数据 达到数据同步,但是其中会有一个问题就是 上述代码的问题 如果一个线程中有一个一直执行的代码块 就叫做热点代码块 jvm的jit就会对其进行优化 以后这个代码块中的不进行写操作的变量 就会直接从副本中拿 就算有其他线程更新了主存中的数据 这个线程对于这个变量依旧从副本中拿。
对于例子来说:由于t0多次读取while(!stop)导致jvm认为while这部分代码块是热点代码 然后jvm里jit编译器就做了优化 因为stop一直只有读操作 所以以后再读取直接读取高速缓存里的stop,但是其他代码的stop还用的还是最新主存拷贝的副本,所以才会出现,有一个线程更新了stop=true但是主线程就是获取不到 但是其他线程都可以获取到。
需要注意的一点是:只有处于热点代码块中的读操作的变量 才会一直从工作内存中获取,但是有写操作的变量 每次会更新到工作内存后还会更新到主存中,大家可以试试创建一个静态变量cnt=0 在while循环中cnt++,然后其他线程在睡眠几秒后持续读取cnt 会发现cnt一直在增加,也就说明cnt确实更新到主存中了

04.然后避免上述例子的问题

volatile的可见性

@Slf4j(topic = "c.Test32")
public class Test32 {
    static volatile Boolean stop = false;
    public static void main(String[] args) {
        new Thread(()->{
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            log.debug("把stop修改为true");
        }).start();

        new Thread(()->{
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("{}",stop);
        }).start();

        foo();
    }
    static void foo(){
        int i=0;
        while (!stop){
            i++;
        }
        log.debug("stopped...c:{}",i);
    }
}

结果

19:35:37.187 c.Test32 [main] - stopped...c:262126608
19:35:37.187 c.Test32 [Thread-0] - 把stop修改为true
19:35:37.279 c.Test32 [Thread-1] - true

解释
可以看出这次成功的使while退出了,原因是 volatile的可见性 会使线程每一次必须从主存中获取数据更新到工作内存中,所以每一次的数据都是最新,所以stop被其他线程更新后,这次 while(!stop)的stop就是true了

synchronized的可见性

@Slf4j(topic = "c.Test32")
public class Test32 {
    static Boolean stop = false;
    static String lock = "";
    public static void main(String[] args) {
        new Thread(()->{
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            log.debug("把stop修改为true");
        }).start();

        new Thread(()->{
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("{}",stop);
        }).start();

        foo();
    }
    static void foo(){
        int i=0;
        while (true){
            synchronized (lock){
                if (stop)
                    break;
            }
            i++;
        }
        log.debug("stopped...c:{}",i);
    }
}

结果

19:47:45.452 c.Test32 [main] - stopped...c:5667729
19:47:45.452 c.Test32 [Thread-0] - 把stop修改为true
19:47:45.546 c.Test32 [Thread-1] - true

解释
可以看出我们利用synchronized也可以实现stop=true。
原因是:在Java内存模型中,synchronized规定,线程在加锁时, 先清空工作内存→在主存中拷贝最新变量的副本到工作内存 →执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。

05.问题

1.在循环里new String 可见性恢复 循环退出,但是我String s = " ",可见性没有恢复。
2.在问题1基础上 我尝试了自己写的类TestKJX里面没有任何内容 我在循环里面创建了new TestKJX() 可见性没有恢复,但是如果我在TestKJX里加了一个final修饰成员变量a并赋初值 此时再new 可见性恢复,如果我没有用final修饰 此时再new 可见性没有恢复
3.在问题2的基础上 我把final改为volatile可见性没有恢复,不过我把new TestKJX()放在while外面 在里面调用a变量 则可见性恢复

以上问题都是关于final的(String类的实现是final修饰value数组),我们可以看出在初始化final时是具有可见性的,但是在初始化之后 就没有了。
所以对应final是不是具有可见性我不确定。

关于可见性我还是有很多疑问是上述理论解决不了的,等我以后了解更多知识之后再来完善这篇文章。

目录
相关文章
|
1月前
|
存储 SQL 缓存
揭秘Java并发核心:深度剖析Java内存模型(JMM)与Volatile关键字的魔法底层,让你的多线程应用无懈可击
【8月更文挑战第4天】Java内存模型(JMM)是Java并发的核心,定义了多线程环境中变量的访问规则,确保原子性、可见性和有序性。JMM区分了主内存与工作内存,以提高性能但可能引入可见性问题。Volatile关键字确保变量的可见性和有序性,其作用于读写操作中插入内存屏障,避免缓存一致性问题。例如,在DCL单例模式中使用Volatile确保实例化过程的可见性。Volatile依赖内存屏障和缓存一致性协议,但不保证原子性,需与其他同步机制配合使用以构建安全的并发程序。
57 0
|
2月前
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
25 0
|
30天前
|
缓存 Java
【多线程面试题二十三】、 说说你对读写锁的了解volatile关键字有什么用?
这篇文章讨论了Java中的`volatile`关键字,解释了它如何保证变量的可见性和禁止指令重排,以及它不能保证复合操作的原子性。
|
2月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
60 1
|
2月前
|
设计模式 缓存 安全
Java面试题:工厂模式与内存泄漏防范?线程安全与volatile关键字的适用性?并发集合与线程池管理问题
Java面试题:工厂模式与内存泄漏防范?线程安全与volatile关键字的适用性?并发集合与线程池管理问题
45 1
|
2月前
|
微服务
多线程内存模型问题之在单例模式中,volatile关键字的作用是什么
多线程内存模型问题之在单例模式中,volatile关键字的作用是什么
|
2月前
线程可见性和关键字volatile
线程可见性和关键字volatile
|
2月前
|
算法 安全 Java
Java面试题:解释JVM中的堆内存分代收集策略,并讨论年轻代和老年代的特点,描述Java中的线程池,并解释线程池的优点,解释Java中的`volatile`关键字的作用和使用场景
Java面试题:解释JVM中的堆内存分代收集策略,并讨论年轻代和老年代的特点,描述Java中的线程池,并解释线程池的优点,解释Java中的`volatile`关键字的作用和使用场景
34 0
|
4月前
|
安全 Java 编译器
Java多线程基础-6:线程安全问题及解决措施,synchronized关键字与volatile关键字(一)
线程安全问题是多线程编程中最典型的一类问题之一。如果多线程环境下代码运行的结果是符合我们预期的,即该结果正是在单线程环境中应该出现的结果,则说这个程序是线程安全的。 通俗来说,线程不安全指的就是某一代码在多线程环境下执行会出现bug,而在单线程环境下执行就不会。线程安全问题本质上是由于线程之间的调度顺序的不确定性,正是这样的不确定性,给我们的代码带来了很多“变数”。 本文将对Java多线程编程中,线程安全问题展开详细的讲解。
77 0
|
4月前
|
Java 编译器
多线程(volatile)
多线程(volatile)
26 0