当前位置: 首页 > news >正文

【JavaEE】了解volatile和wait、notify(三)

volatile

volatile关键字的重要功能:保证内存可见性

在多线程中常常会出现的线程安全问题:

1.内存可见性

计算机运行的程序代码中,经常要访问数据,这些依赖的数据往往会存储在内存中(也就是会定义一个变量,变量中存放的数据就是经常要访问的数据,而这个变量就在内存中)。CPU使用这个变量时,就会把这个内存中的数据先读出来,放在CPU寄存器中,再参与运算。

CPU读取内存这个操作时非常慢的(慢只是相对而言),对CPU而言,它的大部分操作都是很快的,一旦操作涉及读/写内存,就导致速度降下来。CPU内部操作 > 读/写寄存器 > 读取高速缓存Cache > 读/写内存 > 读硬盘。

为了解决读取内存速度慢这个问题,编译器会对代码做出优化,把一些本来要读内存的操作优化程读取寄存器,这样一来就可以减少读内存的次数,提高效率,这个就是内存的可见性。

我们用一个代码详细的解释:

public class Demo01 {private static int Q = 0;//全局变量public static void main(String[] args) {Thread t1 = new Thread(()->{//该线程不断的判断Q的值是否有变化while (Q == 0){}System.out.println("t1结束");});Thread t2 = new Thread(()->{System.out.println("请输入Q的值:");Scanner s = new Scanner(System.in);Q = s.nextInt();});t1.start();t2.start();}
}

解释以上代码:代码想表达的意思是线程一在不断的判断此时此刻的Q是否为0,而线程二在修改Q的值。正常情况下如果我们将Q的值修改成非零那么线程一就不再循环判断,并且结束线程一。如下图我们运行的结果并非是我们所预期的效果,当预期于结果不同时就是一个线程安全的问题。

也就是说一个线程读,一个线程写也会出现线程安全问题,这就是内存可见性引起的。那么我们深入分析一下每一步过程:

在此之前我们也得知道这个线程一是分两步的,第一步:load读取内存中的Q值到寄存器中;第二步:通过cmp指令比较寄存器的值是否为0,决定是否继续循环;

由于这个循环的速度非常快,短时间内就会进行大量循环,也就是进行大量的load和cmp。此时,编译器就发现进行这么多次的load,结果都是一样的,读取内存速度慢费时间(完成一次load等价于上万次cmp),于是,编译器就做了一个决定,只读一次内存中的数据,后面不再重复读,直接从寄存器中取Q的值(这样就感知不到Q在变化)。以至于线程二修改了Q的值,线程一也没有结束的原因,于是就出现了以上的bug。

解决内存可见性问题

在多线程环境下,编译器对于是否进行这样的优化判断不一定准确,就需要我们通过volatile关键字告诉编译器不要优化,不管是否优化都可以通过volatile预防这种优化(也叫保证内存可见性),并且volatile不能保证原子性。也就是开头所说的保证内存可见性。同样的,我们可以也在循环中添加sheep休眠为了减缓速度也可以达到volatile的效果,只是原因不同了减缓速度使开销变小了,编译器就没有必要进行优化(这种方案不可靠,在编写代码时,我们无法确定编译器是否对我们的程序进行了优化),最好的方案就是是由volatile关键字。

wait 和 notify

wait 和 notify 执行时所做的事:

1.释放当前的锁

2.让线程进入阻塞

3.当线程被唤醒时,重新获得锁

如果在一个线程中没有锁就直接调用 wait 会抛出异常(非法监视),所以在调用 wait/notify 前该线程需要加锁

wait 和 notify 是相互使用,wait 是阻塞状态,而 notify 则是唤醒 其中一个被阻塞的线程。

public class Demo01 {public static void main(String[] args) throws InterruptedException {Object lock = new Object();Thread t1 = new Thread(()->{synchronized (lock){System.out.println("开始");try {lock.wait();//此时的锁被释放,目前为阻塞状态,等待notify唤醒} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("结束");}});Thread t2 = new Thread(()->{try {Thread.sleep(3000);//先让t1线程获得锁} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (lock){lock.notify();}});t1.start();t2.start();}
}

notigy 是一次唤醒一个,而 notifyAll 则是唤醒当前所有被阻塞状态的线程。但是就算线程被唤醒,需要重新获得锁,如果是同一把锁还是存在锁竞争。

并且wait 和 notify 可以避免线程饿死,正是它的功能 “释放当前锁”。(线程饿死:一个或多个线程因为无法获取到必要的资源(通常是 CPU 时间片 ,或者是锁资源),而长时间无法执行,处于饥饿等待状态。)

wait 也可以添加超时限制,也就是当阻塞时间超过该限制时,线程不再阻塞而是继续往后执行。

join的用法:哪个线程调用join,就等那个线程结束后再往后执行代码。

http://www.xdnf.cn/news/1368685.html

相关文章:

  • 算法题打卡力扣第209题:长度最小的子数组(mid)
  • 【强化学习】区分理解: 时序差分(TD)、蒙特卡洛(MC)、动态规划(DP)
  • THM El Bandito
  • 使用C++与Qt6,在windows上打造MacOS风格桌面应用窗口
  • SELinux
  • Mac测试端口连接的几种方式
  • 【制作100个Unity游戏】从零开始构建类《月圆之夜》《杀戮尖塔》的卡牌游戏(附带项目源码)
  • CSS 结构伪类选择器
  • C语言开发入门教程:从环境搭建到第一个程序
  • 【lucene】SpanNotQuery 存在的意义
  • 国产化Excel开发组件Spire.XLS教程:Python 读取 CSV 文件,从基础到进阶指南
  • 一文看懂@Bean注解的原理
  • 【C++】用哈希表封装实现unordered_set和unordered_map
  • Ubuntu 操作系统
  • 自动化测试概念与 Web 自动化实战(基于 Selenium)
  • Tensor常见操作
  • pycharm 远程连接服务器报错
  • Java基础第二课:hello word
  • 160.在 Vue3 中用 OpenLayers 解决国内 OpenStreetMap 地图加载不出来的问题
  • 从行业智能体到一站式开发平台,移动云推动AI智能体规模化落地
  • Windows 命令行:mkdir 命令
  • 三菱FX5U PLC访问字变量的某一位
  • Elasticsearch精准匹配与全文检索对比
  • 如何从零开始学习黑客技术?网络安全入门指南
  • 读《精益数据分析》:用户行为热力图
  • 【算法--链表题2】19.删除链表的倒数第 N 个节点:通俗详解
  • 腾讯开源OpenTenBase深度实践:企业级分布式HTAP数据库部署全攻略
  • Qt数据结构与编码技巧全解析
  • Spring - 文件上传与下载:真正的企业开发高频需求——Spring Boot文件上传与下载全场景实践指南
  • 基于stm32的物联网OneNet火灾报警系统