首页 > 编程语言 >JavaEE 初阶(8)——多线程6之线程安全下-->volatile

JavaEE 初阶(8)——多线程6之线程安全下-->volatile

时间:2024-07-27 23:55:10浏览次数:12  
标签:初阶 Thread -- t1 编译器 volatile 内存 线程 多线程

之前,我们在  JavaEE 初阶(6)  这篇文章中,谈到过引起线程不安全的原因,初步了解了 “可见性”  “Java内存模型”  “指令重排序” ,本章讲解 volatile 会涉及到这三个知识点,详细内容可以参考  JavaEE 初阶(6)  这篇文章。

目录

一. 引入

二. volatile关键字 

a. 保证“可见性” 

b. 保证“有序性”

c. 不保证“原子性”


一. 引入

首先,让我们看一个具体的代码实例:

public class VolatileDemo {
    private static int n = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(n == 0){
                //啥都不写
            }
            System.out.println("t1 线程循环结束");
        });

        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个数字");
            n = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

运行结果: 


*当我们输入 任何一个非0 数字时, t1 线程并没有结束。这是为什么呢?

我们可以通过 jconsole  发现,此时 t1 线程(Thread-0)依旧是运行状态,与预期结果不符,说明此时出现了bug,同样是线程安全问题!!


此时,问题就出现在while循环这个地方 :

 每次循环,都要执行一个 n==0 这样的判断。

JVM 执行的时候,会有两个选择:

  1. 从内存中读取数据到寄存器中,再执行类似cmp(比较)指令 --> 读取内存,相比读取寄存器,操作速度非常慢
  2. 直接执行类似cmp(比较)指令,比较0与寄存器中的值,省略第一步 --> 操作速度非常快

    此时 JVM执行这个代码的时候,发现每次循环的过程中 --> 1)执行1操作的开销非常大 2)感觉每次执行1操作的结果都一样 3)JVM根本没有意识到 用户可能在未来会修改n

于是JVM就做了一个大胆的操作——直接把操作1优化掉了(即每次循环,不会重新读取内存中的数据,而是直接读取 寄存器 / cache缓存 中的数据)

    当JVM做出上述决定之后,此时意味着,循环的开销大幅度降低了!但是,当用户修改 n 的值时,内存中的 n 已经改变了,但是由于 t1 线程每次循环,不会真的读内存,感知不到 n 的改变。内存中的 n 对于 t1 线程来说,是 "不可见” 的,这样就引起了bug --> “内存可见性问题

"内存可见性"问题 本质上是 编译器 / JVM 对代码进行优化的时候,优化出 bug ~~

  • 如果代码是单线程的,编译器 / JVM 的代码优化一般是非常准确的,优化之后,不会影响到逻辑。
  • 但是代码如果是多线程的,编译器 / JVM 的代码优化,就可能出现误判(编译器 / JVM 的bug)导致不该优化的地方也给优化了,于是就造成了内存可见性问题了。

 * 编译器为啥要做上述的代码优化?为啥不老老实实按照程序员写的代码一板一眼执行? 

编译器优化原因:

  1. 提高执行速度:编译器优化可以减少程序运行时的指令数量,从而减少CPU的负担,提高程序的执行速度
  2. 减少内存使用:优化可以减少程序在运行时占用的内存空间,例如通过消除死代码、合并变量等方法
  3. 提高代码效率:编译器优化可以帮助提高代码的执行效率

编译器优化 本身也是一个复杂的话题,站在一个外行人的角度,很难判定某个代码是否是优化的。

编译器优化的效果是非常明显的。我们之前的服务器启动的时候,开启优化,启动时间是10min(当时我们的程序,要从硬盘上加载 100多个G 的数据到内存里)如果不开启优化,启动时间是30min+....


我们可以在 while循环 中加入 sleep操作,让 t1 线程可以感知到 t2 线程的修改:

public class VolatileDemo2 {
    private static int n = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(n == 0){
                //此处即使sleep时间非常短,但是刚才的内存可见性问题就消失了
                //t2 的修改可以被 t1 感知到
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 线程循环结束");
        });

        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个数字");
            n = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

 运行结果:

   说明加入sleep之后,刚才谈到的针对读取 n 内存数据的优化操作,不再进行了......

   但是,和读内存相比,sleep开销是更大的,远远超过了读取内存,就算把读取内存操作优化掉,也是没有意义的~~  

* 如果代码的循环里没有 sleep,又希望代码能够没有 bug --> volatile关键字 

public class VolatileDemo3 {
    private static volatile int n = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(n == 0){
                //啥都不写
            }
            System.out.println("t1 线程循环结束");
        });

        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个数字");
            n = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

运行结果:

 

二. volatile关键字 

volatile关键字:用于修饰变量的类型限定符。它提供了一种轻量级的同步机制,以确保共享变量的“可见性”和“有序性”,但不保证“原子性”。针对一个变量,一个线程修改,一个线程读取时,考虑使用 volatile 修饰这个变量。

a. 保证“可见性” 

当一个变量被声明为 volatile 时,它会确保对该变量的写操作立即被其他线程看到。

代码在写入 volatile 修饰的变量的时候

  • 改变线程 “工作内存” 中volatile变量副本的值
  • 将改变后的副本的值从 “工作内存” 刷新到 “主内存” 

代码在读取 volatile 修饰的变量的时候

  • 从 “主内存” 中读取volatile变量的最新值到线程的 “工作内存” 中
  • 从 “工作内存” 中读取volatile变量的副本

“主内存”(main memory):实际上是 “内存”(其实main不用翻译成“主”,main memory 本身就可以译为“内存”)

“工作内存”(working memory):不是实际意义上的 “内存”,实际上是 CPU的寄存器或者CPU的缓存cache(其实翻译成 “工作存储区” 更为合适——这个存储区 是用来进行接下来的运算和逻辑判断的......因为Java语言 本身就是想让程序员 不必太关心底层硬件设备的细节和差异,并且cpu结构 也在持续发生变化,所以官方文档干脆不谈硬件细节了,直接使用work memory术语来表示了)

前面我们讨论“内存可见性”时说了,直接访问“工作内存”,速度非常快,但是可能出现数据不一致的情况。

加上 volatile,强制读写内存,速度是慢了,但是数据变的更准确了

 * 编译器进行上述优化的前提是:编译器认为,针对这个变量的频繁读取,结果都是固定的。当引入 volatile 时,编译器生成这个代码的时候,就会给这个变量的读取操作附近生成一些特殊的指令,称为“内存屏障” --> 相当于告诉编译器说,这个变量是 “易变” 的。 后续JVM执行到这些特殊指令,编译器就会停止上述的优化,确保每次循环都是从内存中重新读取数据。 

   编译器的开发者 知道这个场景中 可能出现误判,于是就把权限交给了程序员,让程序员能够部分干预到优化的进行,让程序员显式地提醒编译器,这里别优化~~

b. 保证“有序性”

volatile 可以防止指令重排序优化。编译器和处理器可能会对指令进行重排序以提高性能,但是当变量声明为 volatile 时,它会作为一个“内存屏障”,保证 volatile 变量前后的操作不会被重排序,从而保证了代码的执行顺序。

c. 不保证“原子性”

需要注意的是, volatile 并不能保证复合操作(如自增、自减或检查后执行逻辑)的原子性。如果需要执行复合操作,仍然需要使用synchronized 

 

标签:初阶,Thread,--,t1,编译器,volatile,内存,线程,多线程
From: https://blog.csdn.net/2301_80243321/article/details/140723557

相关文章

  • JavaEE 初阶(7)——多线程5之线程安全中 -->“死锁”
    目录一.什么是“死锁”二.产生死锁的场景  场景1:一个线程连续加锁 场景2:两个线程两把锁场景3:N个线程M把锁 三.产生死锁的四个必要条件(缺一不可)四. Java标准库中的线程安全类一.什么是“死锁”并非是synchronized就一定线程安全,还要看代码具体咋写。到底......
  • JavaEE 初阶(9)——多线程7之 wait 和 notify
    目录一.监视器锁与监视器二.wait()  三.notify()和notifyAll()3.1notify() 3.2notifyAll()3.3wait等待 和sleep休眠的对比(面试题)wait(等待)/notify(通知)由于线程在操作系统上是“随机调度,抢占式执行”的,因此线程之间执行的先后顺序难以预知。但是......
  • C++字体库开发之go语言绑定六
    export.h#defineFONTVIEW_API__declspec(dllimport)typedefvoid*GoFontSetPtr;#ifdef__cplusplusextern"C"{#endifFONTVIEW_APIGoFontSetPtropenFontSet(constchar*stream,size_tsize);FONTVIEW_APIvoidfreeFontSet(GoFontSetPtrfontset);......
  • QT实现windows窗口内嵌
    appQSharedMemoryshared("appID");//attach成功表示已经存在该内存了,表示当前存在实例if(shared.attach())//共享内存被占用则直接返回return0;MainWindoww;w.show();qulonglongwinID=(qulonglong)w.winId();shared.cr......
  • 每日一知识点 - Java常用关键字
    目录......
  • C语言内存函数精讲
    目录引言1.内存分配函数malloc2.内存释放函数free3.内存拷贝函数memcpy4.内存移动函数memmove5.内存设置函数memset6.内存比较函数memcmp总结 引言在C语言编程中,内存管理是核心技能之一。C语言提供了一系列内存操作函数,这些函数在动态内存分配、数据拷贝和比较等方......
  • AtCoder Beginner Contest 364
    A-GluttonTakahashi(abc364A)题目大意给定\(n\)个字符串,问是否有两个相邻的sweet。解题思路遍历判断当前字符串与上一个字符串是否都为sweet即可。神奇的代码#include<bits/stdc++.h>usingnamespacestd;usingLL=longlong;intmain(void){ios::sync_......
  • ubuntu环境中安装conda
    ​1、下载资源        我开始安装的时候,也是在网上搜索安装方法。会发现有很多下载链接。可能当时作者写博客时可以后,反正现在我发现用不了。所以我们这里直接使用官网,但是这样会导致下载比较慢。并且latest按照我的理解即使最后的,最新的。    建议新建一个单......
  • python---json文件写入
    ​ 使用到的知识点:os模块执行linux指令、json.dump()、withopenasf代码实现importsysimportosimportjson #向json文件file中添加内容data,其中data的类型为字典defwrite_json(file,data):    #如果文件存在,则删除    if(os.path.exists(fi......
  • vscode-react-javascript-snippets
    SnippetsSnippetsinfoEveryspaceinside {} and () meansthatthisispushedintonextline:) $ representeachstepafter tab.TypeScript hasowncomponentsandownsnippets.Usesearchorjusttype ts beforeeverycomponentsnippet.I.E. t......